Add support for LTX 2.5
This commit is contained in:
@@ -7,7 +7,7 @@ from .wan22 import Wan225bModel, Wan2214bModel, Wan2214bI2VModel
|
|||||||
from .qwen_image import QwenImageModel, QwenImageEditModel, QwenImageEditPlusModel
|
from .qwen_image import QwenImageModel, QwenImageEditModel, QwenImageEditPlusModel
|
||||||
from .flux2 import Flux2Model, Flux2Klein4BModel, Flux2Klein9BModel
|
from .flux2 import Flux2Model, Flux2Klein4BModel, Flux2Klein9BModel
|
||||||
from .z_image import ZImageModel
|
from .z_image import ZImageModel
|
||||||
from .ltx2 import LTX2Model, LTX23Model
|
from .ltx2 import LTX2Model, LTX23Model, LTX25Model
|
||||||
from .zeta_chroma import ZetaChromaModel
|
from .zeta_chroma import ZetaChromaModel
|
||||||
from .ernie_image import ErnieImageModel
|
from .ernie_image import ErnieImageModel
|
||||||
from .nucleus_image import NucleusImageModel
|
from .nucleus_image import NucleusImageModel
|
||||||
@@ -40,6 +40,7 @@ AI_TOOLKIT_MODELS = [
|
|||||||
ZImageModel,
|
ZImageModel,
|
||||||
LTX2Model,
|
LTX2Model,
|
||||||
LTX23Model,
|
LTX23Model,
|
||||||
|
LTX25Model,
|
||||||
Flux2Klein4BModel,
|
Flux2Klein4BModel,
|
||||||
Flux2Klein9BModel,
|
Flux2Klein9BModel,
|
||||||
ZetaChromaModel,
|
ZetaChromaModel,
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
from .ltx2 import LTX2Model, LTX23Model
|
from .ltx2 import LTX2Model, LTX23Model, LTX25Model
|
||||||
@@ -320,9 +320,12 @@ def get_ltx2_transformer_config(
|
|||||||
}
|
}
|
||||||
rename_dict = LTX_2_0_TRANSFORMER_KEYS_RENAME_DICT
|
rename_dict = LTX_2_0_TRANSFORMER_KEYS_RENAME_DICT
|
||||||
special_keys_remap = LTX_2_0_TRANSFORMER_SPECIAL_KEYS_REMAP
|
special_keys_remap = LTX_2_0_TRANSFORMER_SPECIAL_KEYS_REMAP
|
||||||
elif version == "2.3":
|
elif version in ("2.3", "2.5"):
|
||||||
|
# LTX-2.5 keeps the 2.3 transformer config; its checkpoint-level deltas
|
||||||
|
# (bias-free video feedforward, keyframes_abs_pos_embedding) are applied
|
||||||
|
# as module surgery in convert_ltx2_transformer.
|
||||||
config = {
|
config = {
|
||||||
"model_id": "Lightricks/LTX-2.3",
|
"model_id": f"Lightricks/LTX-{version}",
|
||||||
"diffusers_config": {
|
"diffusers_config": {
|
||||||
"in_channels": 128,
|
"in_channels": 128,
|
||||||
"out_channels": 128,
|
"out_channels": 128,
|
||||||
@@ -423,9 +426,11 @@ def get_ltx2_connectors_config(
|
|||||||
}
|
}
|
||||||
rename_dict = LTX_2_0_CONNECTORS_KEYS_RENAME_DICT
|
rename_dict = LTX_2_0_CONNECTORS_KEYS_RENAME_DICT
|
||||||
special_keys_remap = LTX_2_0_CONNECTORS_SPECIAL_KEYS_REMAP
|
special_keys_remap = LTX_2_0_CONNECTORS_SPECIAL_KEYS_REMAP
|
||||||
elif version == "2.3":
|
elif version in ("2.3", "2.5"):
|
||||||
|
# LTX-2.5's Gemma-4 12B text encoder has the same hidden size and layer
|
||||||
|
# count as 2.3's Gemma-3 12B, so the connector dims are unchanged.
|
||||||
config = {
|
config = {
|
||||||
"model_id": "Lightricks/LTX-2.3",
|
"model_id": f"Lightricks/LTX-{version}",
|
||||||
"diffusers_config": {
|
"diffusers_config": {
|
||||||
"caption_channels": 3840,
|
"caption_channels": 3840,
|
||||||
"text_proj_in_factor": 49,
|
"text_proj_in_factor": 49,
|
||||||
@@ -457,7 +462,7 @@ def get_ltx2_connectors_config(
|
|||||||
|
|
||||||
|
|
||||||
def convert_ltx2_transformer(
|
def convert_ltx2_transformer(
|
||||||
original_state_dict: Dict[str, Any], version: str = "2.0"
|
original_state_dict: Dict[str, Any], version: str = "2.0", load: bool = True
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
config, rename_dict, special_keys_remap = get_ltx2_transformer_config(version)
|
config, rename_dict, special_keys_remap = get_ltx2_transformer_config(version)
|
||||||
diffusers_config = config["diffusers_config"]
|
diffusers_config = config["diffusers_config"]
|
||||||
@@ -484,12 +489,38 @@ def convert_ltx2_transformer(
|
|||||||
continue
|
continue
|
||||||
handler_fn_inplace(key, transformer_state_dict)
|
handler_fn_inplace(key, transformer_state_dict)
|
||||||
|
|
||||||
|
if version == "2.5":
|
||||||
|
# 2.5 checkpoints drop the video feedforward biases (audio_ff keeps
|
||||||
|
# them) and carry a learned keyframe absolute-position embedding; the
|
||||||
|
# pinned diffusers class predates both, so rebuild those pieces here.
|
||||||
|
# The keyframe embedding is not consumed by the regular forward pass.
|
||||||
|
for block in transformer.transformer_blocks:
|
||||||
|
proj = block.ff.net[0].proj
|
||||||
|
block.ff.net[0].proj = torch.nn.Linear(
|
||||||
|
proj.in_features, proj.out_features, bias=False, device="meta"
|
||||||
|
)
|
||||||
|
out = block.ff.net[2]
|
||||||
|
block.ff.net[2] = torch.nn.Linear(
|
||||||
|
out.in_features, out.out_features, bias=False, device="meta"
|
||||||
|
)
|
||||||
|
keyframes_embedding = transformer_state_dict.get(
|
||||||
|
"keyframes_abs_pos_embedding", None
|
||||||
|
)
|
||||||
|
if keyframes_embedding is not None:
|
||||||
|
transformer.keyframes_abs_pos_embedding = torch.nn.Parameter(
|
||||||
|
torch.empty_like(keyframes_embedding, device="meta"),
|
||||||
|
requires_grad=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not load:
|
||||||
|
return transformer, transformer_state_dict
|
||||||
|
|
||||||
transformer.load_state_dict(transformer_state_dict, strict=True, assign=True)
|
transformer.load_state_dict(transformer_state_dict, strict=True, assign=True)
|
||||||
return transformer
|
return transformer
|
||||||
|
|
||||||
|
|
||||||
def convert_ltx2_connectors(
|
def convert_ltx2_connectors(
|
||||||
original_state_dict: Dict[str, Any], version: str = "2.0"
|
original_state_dict: Dict[str, Any], version: str = "2.0", load: bool = True
|
||||||
) -> LTX2TextConnectors:
|
) -> LTX2TextConnectors:
|
||||||
config, rename_dict, special_keys_remap = get_ltx2_connectors_config(version)
|
config, rename_dict, special_keys_remap = get_ltx2_connectors_config(version)
|
||||||
diffusers_config = config["diffusers_config"]
|
diffusers_config = config["diffusers_config"]
|
||||||
@@ -515,6 +546,9 @@ def convert_ltx2_connectors(
|
|||||||
continue
|
continue
|
||||||
handler_fn_inplace(key, connector_state_dict)
|
handler_fn_inplace(key, connector_state_dict)
|
||||||
|
|
||||||
|
if not load:
|
||||||
|
return connectors, connector_state_dict
|
||||||
|
|
||||||
connectors.load_state_dict(connector_state_dict, strict=True, assign=True)
|
connectors.load_state_dict(connector_state_dict, strict=True, assign=True)
|
||||||
return connectors
|
return connectors
|
||||||
|
|
||||||
@@ -607,9 +641,12 @@ def get_ltx2_video_vae_config(
|
|||||||
}
|
}
|
||||||
rename_dict = LTX_2_0_VIDEO_VAE_RENAME_DICT
|
rename_dict = LTX_2_0_VIDEO_VAE_RENAME_DICT
|
||||||
special_keys_remap = LTX_2_0_VAE_SPECIAL_KEYS_REMAP
|
special_keys_remap = LTX_2_0_VAE_SPECIAL_KEYS_REMAP
|
||||||
elif version == "2.3":
|
elif version in ("2.3", "2.5"):
|
||||||
|
# 2.5's conv video VAE ("-conv-" file) is architecturally identical to
|
||||||
|
# 2.3's; the default 2.5 vae file is a new diffusion-decoder VAE that
|
||||||
|
# is not supported here.
|
||||||
config = {
|
config = {
|
||||||
"model_id": "Lightricks/LTX-2.3",
|
"model_id": f"Lightricks/LTX-{version}",
|
||||||
"diffusers_config": {
|
"diffusers_config": {
|
||||||
"in_channels": 3,
|
"in_channels": 3,
|
||||||
"out_channels": 3,
|
"out_channels": 3,
|
||||||
@@ -718,9 +755,9 @@ def get_ltx2_audio_vae_config(
|
|||||||
}
|
}
|
||||||
rename_dict = LTX_2_0_AUDIO_VAE_RENAME_DICT
|
rename_dict = LTX_2_0_AUDIO_VAE_RENAME_DICT
|
||||||
special_keys_remap = LTX_2_0_AUDIO_VAE_SPECIAL_KEYS_REMAP
|
special_keys_remap = LTX_2_0_AUDIO_VAE_SPECIAL_KEYS_REMAP
|
||||||
elif version == "2.3":
|
elif version in ("2.3", "2.5"):
|
||||||
config = {
|
config = {
|
||||||
"model_id": "Lightricks/LTX-2.3",
|
"model_id": f"Lightricks/LTX-{version}",
|
||||||
"diffusers_config": {
|
"diffusers_config": {
|
||||||
"base_channels": 128,
|
"base_channels": 128,
|
||||||
"output_channels": 2,
|
"output_channels": 2,
|
||||||
@@ -798,9 +835,9 @@ def get_ltx2_vocoder_config(
|
|||||||
}
|
}
|
||||||
rename_dict = LTX_2_0_VOCODER_RENAME_DICT
|
rename_dict = LTX_2_0_VOCODER_RENAME_DICT
|
||||||
special_keys_remap = LTX_2_0_VOCODER_SPECIAL_KEYS_REMAP
|
special_keys_remap = LTX_2_0_VOCODER_SPECIAL_KEYS_REMAP
|
||||||
elif version == "2.3":
|
elif version in ("2.3", "2.5"):
|
||||||
config = {
|
config = {
|
||||||
"model_id": "Lightricks/LTX-2.3",
|
"model_id": f"Lightricks/LTX-{version}",
|
||||||
"diffusers_config": {
|
"diffusers_config": {
|
||||||
"in_channels": 128,
|
"in_channels": 128,
|
||||||
"hidden_channels": 1536,
|
"hidden_channels": 1536,
|
||||||
@@ -848,7 +885,7 @@ def convert_ltx2_vocoder(
|
|||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
config, rename_dict, special_keys_remap = get_ltx2_vocoder_config(version)
|
config, rename_dict, special_keys_remap = get_ltx2_vocoder_config(version)
|
||||||
diffusers_config = config["diffusers_config"]
|
diffusers_config = config["diffusers_config"]
|
||||||
if version == "2.3":
|
if version in ("2.3", "2.5"):
|
||||||
vocoder_cls = LTX2VocoderWithBWE
|
vocoder_cls = LTX2VocoderWithBWE
|
||||||
else:
|
else:
|
||||||
vocoder_cls = LTX2Vocoder
|
vocoder_cls = LTX2Vocoder
|
||||||
@@ -1030,7 +1067,7 @@ def convert_lora_original_to_diffusers(
|
|||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
out: Dict[str, Any] = {}
|
out: Dict[str, Any] = {}
|
||||||
rename_dict = LTX_2_0_TRANSFORMER_KEYS_RENAME_DICT
|
rename_dict = LTX_2_0_TRANSFORMER_KEYS_RENAME_DICT
|
||||||
if version == "2.3":
|
if version in ("2.3", "2.5"):
|
||||||
rename_dict = LTX_2_3_TRANSFORMER_KEYS_RENAME_DICT
|
rename_dict = LTX_2_3_TRANSFORMER_KEYS_RENAME_DICT
|
||||||
|
|
||||||
for k, v in lora_state_dict.items():
|
for k, v in lora_state_dict.items():
|
||||||
@@ -1064,7 +1101,7 @@ def convert_lora_diffusers_to_original(
|
|||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
out: Dict[str, Any] = {}
|
out: Dict[str, Any] = {}
|
||||||
rename_dict = LTX_2_0_TRANSFORMER_KEYS_RENAME_DICT
|
rename_dict = LTX_2_0_TRANSFORMER_KEYS_RENAME_DICT
|
||||||
if version == "2.3":
|
if version in ("2.3", "2.5"):
|
||||||
rename_dict = LTX_2_3_TRANSFORMER_KEYS_RENAME_DICT
|
rename_dict = LTX_2_3_TRANSFORMER_KEYS_RENAME_DICT
|
||||||
|
|
||||||
inv_rename = {v: k for k, v in rename_dict.items()}
|
inv_rename = {v: k for k, v in rename_dict.items()}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
@@ -508,7 +509,7 @@ class LTX2Model(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
vocoder_cls = LTX2Vocoder
|
vocoder_cls = LTX2Vocoder
|
||||||
if self.ltx_version == "2.3":
|
if self.ltx_version in ("2.3", "2.5"):
|
||||||
vocoder_cls = LTX2VocoderWithBWE
|
vocoder_cls = LTX2VocoderWithBWE
|
||||||
|
|
||||||
vocoder = vocoder_cls.from_pretrained(
|
vocoder = vocoder_cls.from_pretrained(
|
||||||
@@ -716,7 +717,7 @@ class LTX2Model(BaseModel):
|
|||||||
conditional_embeds = self.pad_embeds(conditional_embeds)
|
conditional_embeds = self.pad_embeds(conditional_embeds)
|
||||||
unconditional_embeds = self.pad_embeds(unconditional_embeds)
|
unconditional_embeds = self.pad_embeds(unconditional_embeds)
|
||||||
|
|
||||||
if self.ltx_version == "2.3":
|
if self.ltx_version in ("2.3", "2.5"):
|
||||||
extra["stg_scale"] = 1.0
|
extra["stg_scale"] = 1.0
|
||||||
extra["modality_scale"] = 3.0
|
extra["modality_scale"] = 3.0
|
||||||
extra["guidance_rescale"] = 0.7
|
extra["guidance_rescale"] = 0.7
|
||||||
@@ -1057,7 +1058,7 @@ class LTX2Model(BaseModel):
|
|||||||
# use_cross_timestep - Whether to use the cross modality (audio is the cross modality of video, and vice versa) sigma when
|
# use_cross_timestep - Whether to use the cross modality (audio is the cross modality of video, and vice versa) sigma when
|
||||||
# calculating the cross attention modulation parameters. `True` is the newer (e.g. LTX-2.3) behavior;
|
# calculating the cross attention modulation parameters. `True` is the newer (e.g. LTX-2.3) behavior;
|
||||||
# `False` is the legacy LTX-2.0 behavior.
|
# `False` is the legacy LTX-2.0 behavior.
|
||||||
use_cross_timestep = self.ltx_version == "2.3"
|
use_cross_timestep = self.ltx_version in ("2.3", "2.5")
|
||||||
|
|
||||||
noise_pred_video, noise_pred_audio = self.transformer(
|
noise_pred_video, noise_pred_audio = self.transformer(
|
||||||
hidden_states=packed_latents,
|
hidden_states=packed_latents,
|
||||||
@@ -1213,3 +1214,396 @@ class LTX23Model(LTX2Model):
|
|||||||
arch = "ltx2.3"
|
arch = "ltx2.3"
|
||||||
ltx_version = "2.3"
|
ltx_version = "2.3"
|
||||||
ltx_te_path = base_te_path
|
ltx_te_path = base_te_path
|
||||||
|
|
||||||
|
|
||||||
|
# LTX-2.5 ships as ComfyUI-style split files (no diffusers folders, no mono
|
||||||
|
# checkpoint). Files are used in place when present under MODELS_PATH and
|
||||||
|
# downloaded to exactly these locations only when missing, so the models
|
||||||
|
# folder stays shareable with a ComfyUI install. The int8 ConvRot files are
|
||||||
|
# the defaults; bf16 variants stay selectable via model_kwargs overrides.
|
||||||
|
COMFY_LTX25_REPO = "Lightricks/LTX-2.5"
|
||||||
|
COMFY_LTX25_FILES = {
|
||||||
|
"dit": "diffusion_models/ltx-2.5-22b-dev-transformer-comfy-int8-convrot.safetensors",
|
||||||
|
"text_encoder": "text_encoders/gemma4-12b-with-proj-ltx-2.5-comfy-int8-convrot.safetensors",
|
||||||
|
# the "-conv-" file is the classic conv VAE; the default 2.5 vae file is a
|
||||||
|
# new diffusion-decoder VAE that diffusers has no class for
|
||||||
|
"video_vae": "vae/ltx-2.5-video-vae-conv-bf16.safetensors",
|
||||||
|
# bundles the BWE vocoder alongside the audio VAE
|
||||||
|
"audio_vae": "vae/ltx-2.5-audio-vae-bf16.safetensors",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LTX25Model(LTX2Model):
|
||||||
|
arch = "ltx2.5"
|
||||||
|
ltx_version = "2.5"
|
||||||
|
ltx_te_path = None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# ComfyUI-style file resolution (mirrors MinimaxH3Model)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@staticmethod
|
||||||
|
def _find_file_recursive(root_dir: str, filename: str) -> Optional[str]:
|
||||||
|
if not os.path.isdir(root_dir):
|
||||||
|
return None
|
||||||
|
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||||||
|
dirnames.sort()
|
||||||
|
if filename in filenames:
|
||||||
|
return os.path.join(dirpath, filename)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _resolve_comfy_file(self, component: str) -> str:
|
||||||
|
"""Find a weight file at its local location, or download it there
|
||||||
|
when (and only when) it is missing.
|
||||||
|
|
||||||
|
Search order: model_kwargs override, the repo-relative path under
|
||||||
|
MODELS_PATH (diffusion_models/, text_encoders/, vae/), the bare
|
||||||
|
filename at the root, any subfolder of the component's category
|
||||||
|
folder (recursive), then the hub — downloaded to the repo-relative
|
||||||
|
path under MODELS_PATH.
|
||||||
|
"""
|
||||||
|
override = self.model_config.model_kwargs.get(f"{component}_path", None)
|
||||||
|
if override is not None:
|
||||||
|
if not os.path.exists(override):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"model_kwargs.{component}_path does not exist: {override}"
|
||||||
|
)
|
||||||
|
return override
|
||||||
|
|
||||||
|
rel_path = COMFY_LTX25_FILES[component]
|
||||||
|
filename = os.path.basename(rel_path)
|
||||||
|
category = os.path.dirname(rel_path)
|
||||||
|
for rel in (rel_path, filename):
|
||||||
|
candidate = os.path.join(MODELS_PATH, rel)
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return candidate
|
||||||
|
found = self._find_file_recursive(os.path.join(MODELS_PATH, category), filename)
|
||||||
|
if found is not None:
|
||||||
|
return found
|
||||||
|
|
||||||
|
repo_id = COMFY_LTX25_REPO
|
||||||
|
name_or_path = self.model_config.name_or_path
|
||||||
|
if (
|
||||||
|
name_or_path
|
||||||
|
and not os.path.exists(name_or_path)
|
||||||
|
and not name_or_path.endswith(".safetensors")
|
||||||
|
and "/" in name_or_path
|
||||||
|
):
|
||||||
|
repo_id = name_or_path
|
||||||
|
self.print_and_status_update(
|
||||||
|
f"Downloading {rel_path} from {repo_id} into {MODELS_PATH}"
|
||||||
|
)
|
||||||
|
return huggingface_hub.hf_hub_download(
|
||||||
|
repo_id=repo_id, filename=rel_path, token=HF_TOKEN, local_dir=MODELS_PATH
|
||||||
|
)
|
||||||
|
|
||||||
|
def _resolve_named_file(self, path: str, component: str) -> str:
|
||||||
|
"""Resolve an explicit .safetensors path: local file, models-folder
|
||||||
|
file, or an 'org/repo/path/file.safetensors' hub path (downloaded
|
||||||
|
into the models folder)."""
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
splits = path.split("/")
|
||||||
|
if len(splits) < 3:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid {component} path: {path}. Must be a local file or "
|
||||||
|
"'repo_id/repo/filename.safetensors' to download from the Hugging Face Hub."
|
||||||
|
)
|
||||||
|
rel_path = "/".join(splits[2:])
|
||||||
|
for candidate in (
|
||||||
|
os.path.join(MODELS_PATH, rel_path),
|
||||||
|
os.path.join(MODELS_PATH, splits[-1]),
|
||||||
|
):
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return candidate
|
||||||
|
return huggingface_hub.hf_hub_download(
|
||||||
|
repo_id="/".join(splits[:2]),
|
||||||
|
filename=rel_path,
|
||||||
|
token=HF_TOKEN,
|
||||||
|
local_dir=MODELS_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _resolve_dit_path(self) -> str:
|
||||||
|
name_or_path = self.model_config.name_or_path
|
||||||
|
if name_or_path and name_or_path.endswith(".safetensors"):
|
||||||
|
return self._resolve_named_file(name_or_path, "transformer")
|
||||||
|
return self._resolve_comfy_file("dit")
|
||||||
|
|
||||||
|
def _resolve_te_path(self) -> str:
|
||||||
|
te_name_or_path = self.model_config.te_name_or_path
|
||||||
|
if te_name_or_path is not None:
|
||||||
|
return self._resolve_named_file(te_name_or_path, "text encoder")
|
||||||
|
return self._resolve_comfy_file("text_encoder")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Loading
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _load_quantized_module(self, module, state_dict, name: str) -> int:
|
||||||
|
"""Attach pre-quantized (int8 ConvRot) linears onto the toolkit's
|
||||||
|
quantization backends and load the rest of the (meta-built) module
|
||||||
|
from the state dict. Works unchanged for bf16 checkpoints, where no
|
||||||
|
quant markers exist and everything strict-loads."""
|
||||||
|
from toolkit.util.comfy_quant_import import import_comfy_quantized_layers
|
||||||
|
from toolkit.util.ostris_quant import OstrisLinear
|
||||||
|
|
||||||
|
state_dict, num_quantized = import_comfy_quantized_layers(
|
||||||
|
module, state_dict, orig_dtype=self.torch_dtype
|
||||||
|
)
|
||||||
|
if num_quantized:
|
||||||
|
self.print_and_status_update(
|
||||||
|
f" - attached {num_quantized} pre-quantized ConvRot layers to {name}"
|
||||||
|
)
|
||||||
|
result = module.load_state_dict(state_dict, assign=True, strict=False)
|
||||||
|
# quantized linears hold their weight as backend buffers and had their
|
||||||
|
# bias assigned by the importer, so both report as "missing" here
|
||||||
|
quantized_param_keys = set()
|
||||||
|
for mod_name, m in module.named_modules():
|
||||||
|
if isinstance(m, OstrisLinear):
|
||||||
|
quantized_param_keys.add(f"{mod_name}.weight")
|
||||||
|
if m.bias is not None:
|
||||||
|
quantized_param_keys.add(f"{mod_name}.bias")
|
||||||
|
bad_missing = [k for k in result.missing_keys if k not in quantized_param_keys]
|
||||||
|
if bad_missing or result.unexpected_keys:
|
||||||
|
raise ValueError(
|
||||||
|
f"LTX-2.5 {name} load mismatch: missing {bad_missing[:8]}, "
|
||||||
|
f"unexpected {result.unexpected_keys[:8]}"
|
||||||
|
)
|
||||||
|
# nothing may be left on the meta device (e.g. a bias the importer
|
||||||
|
# should have filled)
|
||||||
|
leftover_meta = [
|
||||||
|
param_name
|
||||||
|
for param_name, p in module.named_parameters()
|
||||||
|
if p.is_meta
|
||||||
|
]
|
||||||
|
if leftover_meta:
|
||||||
|
raise ValueError(
|
||||||
|
f"LTX-2.5 {name} load left meta parameters: {leftover_meta[:8]}"
|
||||||
|
)
|
||||||
|
return num_quantized
|
||||||
|
|
||||||
|
def _load_gemma4_text_encoder(self, te_path: str, te_state_dict: dict):
|
||||||
|
"""Build the Gemma-4 12B text stack from the single comfy file. Only
|
||||||
|
the text decoder is loaded — the unified checkpoint's vision/audio
|
||||||
|
tower pieces and the connector projections are used elsewhere or
|
||||||
|
dropped, matching how the Gemma-3 vision tower was discarded."""
|
||||||
|
from safetensors import safe_open
|
||||||
|
from transformers import Gemma4TextConfig
|
||||||
|
from transformers.models.gemma4.modeling_gemma4 import Gemma4TextModel
|
||||||
|
|
||||||
|
with safe_open(te_path, framework="pt") as f:
|
||||||
|
metadata = f.metadata() or {}
|
||||||
|
gemma_config = json.loads(metadata["gemma_config"])
|
||||||
|
text_config = {
|
||||||
|
k: v for k, v in gemma_config["text_config"].items() if k != "dtype"
|
||||||
|
}
|
||||||
|
|
||||||
|
with init_empty_weights():
|
||||||
|
text_encoder = Gemma4TextModel(Gemma4TextConfig(**text_config))
|
||||||
|
|
||||||
|
def strip_model_prefix(key: str) -> str:
|
||||||
|
return key[len("model.") :] if key.startswith("model.") else key
|
||||||
|
|
||||||
|
te_sd = {
|
||||||
|
strip_model_prefix(k): v
|
||||||
|
for k, v in te_state_dict.items()
|
||||||
|
if k.startswith("model.")
|
||||||
|
}
|
||||||
|
num_quantized = self._load_quantized_module(text_encoder, te_sd, "text encoder")
|
||||||
|
return text_encoder, num_quantized
|
||||||
|
|
||||||
|
def _load_gemma4_tokenizer(self, te_path: str, te_state_dict: dict):
|
||||||
|
"""The comfy file embeds the tokenizer and its configs as uint8
|
||||||
|
tensors; extract them next to the file once and load from there."""
|
||||||
|
from transformers import AutoTokenizer
|
||||||
|
|
||||||
|
assets_dir = os.path.splitext(te_path)[0] + "_hf_assets"
|
||||||
|
assets = {
|
||||||
|
"tokenizer.json": "tokenizer_json",
|
||||||
|
"tokenizer_config.json": "hf_asset__tokenizer_config.json",
|
||||||
|
"chat_template.jinja": "hf_asset__chat_template.jinja",
|
||||||
|
}
|
||||||
|
os.makedirs(assets_dir, exist_ok=True)
|
||||||
|
for filename, tensor_key in assets.items():
|
||||||
|
out_path = os.path.join(assets_dir, filename)
|
||||||
|
blob = te_state_dict.get(tensor_key, None)
|
||||||
|
if blob is None or os.path.exists(out_path):
|
||||||
|
continue
|
||||||
|
with open(out_path, "wb") as f:
|
||||||
|
f.write(bytes(blob.cpu().numpy().tobytes()))
|
||||||
|
# the embedded tokenizer.json has an empty post-processor (ComfyUI
|
||||||
|
# prepends BOS in its own wrapper); restore the standard Gemma
|
||||||
|
# behavior so blank prompts still yield a token
|
||||||
|
return AutoTokenizer.from_pretrained(assets_dir, add_bos_token=True)
|
||||||
|
|
||||||
|
def load_model(self):
|
||||||
|
dtype = self.torch_dtype
|
||||||
|
self.print_and_status_update("Loading LTX-2.5 model")
|
||||||
|
|
||||||
|
# ---- transformer + embedding connectors (one comfy file) ----
|
||||||
|
dit_path = self._resolve_dit_path()
|
||||||
|
te_path = self._resolve_te_path()
|
||||||
|
self.print_and_status_update(
|
||||||
|
f"Loading transformer from {os.path.basename(dit_path)}"
|
||||||
|
)
|
||||||
|
combined = load_file(dit_path)
|
||||||
|
dit_sd = get_model_state_dict_from_combined_ckpt(combined, dit_prefix)
|
||||||
|
del combined
|
||||||
|
|
||||||
|
# the per-modality text projections ride in the text encoder file but
|
||||||
|
# belong to the connectors module
|
||||||
|
te_state_dict = load_file(te_path)
|
||||||
|
for key in list(te_state_dict.keys()):
|
||||||
|
if key.startswith("text_embedding_projection."):
|
||||||
|
dit_sd[key] = te_state_dict.pop(key)
|
||||||
|
|
||||||
|
transformer, transformer_sd = convert_ltx2_transformer(
|
||||||
|
dit_sd, version=self.ltx_version, load=False
|
||||||
|
)
|
||||||
|
num_quantized_dit = self._load_quantized_module(
|
||||||
|
transformer, transformer_sd, "transformer"
|
||||||
|
)
|
||||||
|
del transformer_sd
|
||||||
|
if num_quantized_dit == 0:
|
||||||
|
transformer = transformer.to(dtype)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
if self.model_config.quantize:
|
||||||
|
if num_quantized_dit:
|
||||||
|
self.print_and_status_update(
|
||||||
|
"Transformer is pre-quantized (ConvRot); skipping quantize"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.print_and_status_update("Quantizing Transformer")
|
||||||
|
quantize_model(self, transformer)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.model_config.layer_offloading
|
||||||
|
and self.model_config.layer_offloading_transformer_percent > 0
|
||||||
|
):
|
||||||
|
ignore_modules = []
|
||||||
|
for block in transformer.transformer_blocks:
|
||||||
|
ignore_modules.append(block.scale_shift_table)
|
||||||
|
ignore_modules.append(block.audio_scale_shift_table)
|
||||||
|
ignore_modules.append(block.video_a2v_cross_attn_scale_shift_table)
|
||||||
|
ignore_modules.append(block.audio_a2v_cross_attn_scale_shift_table)
|
||||||
|
ignore_modules.append(transformer.scale_shift_table)
|
||||||
|
ignore_modules.append(transformer.audio_scale_shift_table)
|
||||||
|
MemoryManager.attach(
|
||||||
|
transformer,
|
||||||
|
self.device_torch,
|
||||||
|
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||||
|
ignore_modules=ignore_modules,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.model_config.low_vram:
|
||||||
|
self.print_and_status_update("Moving transformer to CPU")
|
||||||
|
transformer.to("cpu")
|
||||||
|
flush()
|
||||||
|
|
||||||
|
self.print_and_status_update("Loading connectors")
|
||||||
|
connectors, connectors_sd = convert_ltx2_connectors(
|
||||||
|
dit_sd, version=self.ltx_version, load=False
|
||||||
|
)
|
||||||
|
num_quantized_connectors = self._load_quantized_module(
|
||||||
|
connectors, connectors_sd, "connectors"
|
||||||
|
)
|
||||||
|
del connectors_sd, dit_sd
|
||||||
|
if num_quantized_connectors == 0:
|
||||||
|
connectors = connectors.to(dtype)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
# ---- text encoder (Gemma-4 12B, single comfy file) ----
|
||||||
|
self.print_and_status_update("Loading text encoder")
|
||||||
|
tokenizer = self._load_gemma4_tokenizer(te_path, te_state_dict)
|
||||||
|
text_encoder, num_quantized_te = self._load_gemma4_text_encoder(
|
||||||
|
te_path, te_state_dict
|
||||||
|
)
|
||||||
|
del te_state_dict
|
||||||
|
flush()
|
||||||
|
|
||||||
|
if self.model_config.quantize_te:
|
||||||
|
if num_quantized_te:
|
||||||
|
self.print_and_status_update(
|
||||||
|
"Text encoder is pre-quantized (ConvRot); skipping quantize"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.print_and_status_update("Quantizing Text Encoder")
|
||||||
|
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
|
||||||
|
freeze(text_encoder)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.model_config.layer_offloading
|
||||||
|
and self.model_config.layer_offloading_text_encoder_percent > 0
|
||||||
|
):
|
||||||
|
MemoryManager.attach(
|
||||||
|
text_encoder,
|
||||||
|
self.device_torch,
|
||||||
|
offload_percent=self.model_config.layer_offloading_text_encoder_percent,
|
||||||
|
ignore_modules=[text_encoder.embed_tokens],
|
||||||
|
)
|
||||||
|
|
||||||
|
text_encoder.to(self.device_torch)
|
||||||
|
text_encoder.requires_grad_(False)
|
||||||
|
text_encoder.eval()
|
||||||
|
flush()
|
||||||
|
|
||||||
|
# ---- VAEs + vocoder ----
|
||||||
|
self.print_and_status_update("Loading VAEs and other components")
|
||||||
|
video_vae_path = self._resolve_comfy_file("video_vae")
|
||||||
|
vae = convert_ltx2_video_vae(
|
||||||
|
load_file(video_vae_path), version=self.ltx_version
|
||||||
|
).to(dtype)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
audio_vae_path = self._resolve_comfy_file("audio_vae")
|
||||||
|
audio_combined = load_file(audio_vae_path)
|
||||||
|
audio_sd = get_model_state_dict_from_combined_ckpt(
|
||||||
|
audio_combined, audio_vae_prefix
|
||||||
|
)
|
||||||
|
audio_vae = convert_ltx2_audio_vae(audio_sd, version=self.ltx_version).to(dtype)
|
||||||
|
vocoder_sd = get_model_state_dict_from_combined_ckpt(
|
||||||
|
audio_combined, vocoder_prefix
|
||||||
|
)
|
||||||
|
vocoder = convert_ltx2_vocoder(vocoder_sd, version=self.ltx_version).to(dtype)
|
||||||
|
del audio_combined, audio_sd, vocoder_sd
|
||||||
|
flush()
|
||||||
|
|
||||||
|
self.noise_scheduler = LTX2Model.get_train_scheduler()
|
||||||
|
|
||||||
|
self.print_and_status_update("Making pipe")
|
||||||
|
pipe: LTX2Pipeline = LTX2Pipeline(
|
||||||
|
scheduler=self.noise_scheduler,
|
||||||
|
vae=vae,
|
||||||
|
audio_vae=audio_vae,
|
||||||
|
text_encoder=None,
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
connectors=connectors,
|
||||||
|
transformer=None,
|
||||||
|
vocoder=vocoder,
|
||||||
|
)
|
||||||
|
pipe.text_encoder = text_encoder
|
||||||
|
pipe.transformer = transformer
|
||||||
|
|
||||||
|
self.print_and_status_update("Preparing Model")
|
||||||
|
text_encoder = [pipe.text_encoder]
|
||||||
|
tokenizer = [pipe.tokenizer]
|
||||||
|
|
||||||
|
if not self.low_vram:
|
||||||
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
self.vae = ComboVae(pipe.vae, pipe.audio_vae)
|
||||||
|
self.text_encoder = text_encoder # list of text encoders
|
||||||
|
self.tokenizer = tokenizer # list of tokenizers
|
||||||
|
self.model = pipe.transformer
|
||||||
|
self.pipeline = pipe
|
||||||
|
|
||||||
|
self.audio_processor = AudioProcessor(
|
||||||
|
sample_rate=pipe.audio_sampling_rate,
|
||||||
|
mel_bins=audio_vae.config.mel_bins,
|
||||||
|
mel_hop_length=pipe.audio_hop_length,
|
||||||
|
n_fft=1024, # todo get this from vae if we can, I couldnt find it.
|
||||||
|
).to(self.device_torch, dtype=torch.float32)
|
||||||
|
|
||||||
|
self.print_and_status_update("Model Loaded")
|
||||||
|
|||||||
@@ -831,6 +831,37 @@ export const modelArchs: ModelArch[] = [
|
|||||||
disableSections: ['network.conv'],
|
disableSections: ['network.conv'],
|
||||||
additionalSections: ['sample.ctrl_img', 'datasets.num_frames', 'model.layer_offloading', 'model.low_vram', 'datasets.do_audio', 'datasets.audio_normalize', 'datasets.audio_preserve_pitch', 'datasets.do_i2v', 'train.audio_loss_multiplier', 'datasets.auto_frame_count'],
|
additionalSections: ['sample.ctrl_img', 'datasets.num_frames', 'model.layer_offloading', 'model.low_vram', 'datasets.do_audio', 'datasets.audio_normalize', 'datasets.audio_preserve_pitch', 'datasets.do_i2v', 'train.audio_loss_multiplier', 'datasets.auto_frame_count'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'ltx2.5',
|
||||||
|
label: 'LTX-2.5',
|
||||||
|
gateUrl: 'https://huggingface.co/Lightricks/LTX-2.5',
|
||||||
|
group: 'video',
|
||||||
|
isVideoModel: true,
|
||||||
|
defaults: {
|
||||||
|
// default updates when [selected, unselected] in the UI
|
||||||
|
// comfy-style split files resolve from/download to the models folder;
|
||||||
|
// the int8 ConvRot dev transformer is the default
|
||||||
|
'config.process[0].model.name_or_path': ['Lightricks/LTX-2.5', defaultNameOrPath],
|
||||||
|
'config.process[0].model.quantize': [false, false],
|
||||||
|
'config.process[0].model.quantize_te': [false, false],
|
||||||
|
'config.process[0].model.low_vram': [true, false],
|
||||||
|
'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
|
||||||
|
'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
|
||||||
|
'config.process[0].sample.num_frames': [121, 1],
|
||||||
|
'config.process[0].sample.fps': [24, 1],
|
||||||
|
'config.process[0].sample.width': [768, 1024],
|
||||||
|
'config.process[0].sample.height': [768, 1024],
|
||||||
|
'config.process[0].train.audio_loss_multiplier': [1.0, undefined],
|
||||||
|
'config.process[0].train.timestep_type': ['weighted', 'sigmoid'],
|
||||||
|
'config.process[0].datasets[x].cache_latents_to_disk': [true, false],
|
||||||
|
'config.process[0].datasets[x].do_i2v': [false, undefined],
|
||||||
|
'config.process[0].datasets[x].do_audio': [true, undefined],
|
||||||
|
'config.process[0].datasets[x].fps': [24, undefined],
|
||||||
|
'config.process[0].datasets[x].auto_frame_count': [false, undefined],
|
||||||
|
},
|
||||||
|
disableSections: ['network.conv'],
|
||||||
|
additionalSections: ['sample.ctrl_img', 'datasets.num_frames', 'model.layer_offloading', 'model.low_vram', 'datasets.do_audio', 'datasets.audio_normalize', 'datasets.audio_preserve_pitch', 'datasets.do_i2v', 'train.audio_loss_multiplier', 'datasets.auto_frame_count'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'flux2_klein_4b',
|
name: 'flux2_klein_4b',
|
||||||
label: 'FLUX.2-klein-base-4B',
|
label: 'FLUX.2-klein-base-4B',
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VERSION = "0.12.13"
|
VERSION = "0.12.14"
|
||||||
|
|||||||
Reference in New Issue
Block a user