cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
346 lines
13 KiB
Python
346 lines
13 KiB
Python
import re
|
|
from typing import (
|
|
Literal,
|
|
Self,
|
|
)
|
|
|
|
from pydantic import Field
|
|
from typing_extensions import Any
|
|
|
|
from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Config_Base, Diffusers_Config_Base
|
|
from invokeai.backend.model_manager.configs.identification_utils import (
|
|
NotAMatchError,
|
|
common_config_paths,
|
|
get_config_dict_or_raise,
|
|
raise_for_class_name,
|
|
raise_for_override_fields,
|
|
raise_if_not_dir,
|
|
raise_if_not_file,
|
|
state_dict_has_any_keys_starting_with,
|
|
)
|
|
from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
|
|
from invokeai.backend.model_manager.taxonomy import (
|
|
BaseModelType,
|
|
ModelFormat,
|
|
ModelType,
|
|
)
|
|
|
|
REGEX_TO_BASE: dict[str, BaseModelType] = {
|
|
r"xl": BaseModelType.StableDiffusionXL,
|
|
r"sd2": BaseModelType.StableDiffusion2,
|
|
r"vae": BaseModelType.StableDiffusion1,
|
|
r"FLUX.1-schnell_ae": BaseModelType.Flux,
|
|
}
|
|
|
|
|
|
def _is_qwen_image_vae(state_dict: dict[str | int, Any]) -> bool:
|
|
"""Check if state dict is a Qwen Image VAE (AutoencoderKLQwenImage).
|
|
|
|
Qwen Image VAE can be identified by:
|
|
1. Diffusers-format encoder/decoder keys (`encoder.conv_in`, `decoder.conv_in`)
|
|
2. 5-dimensional convolution weights (3D causal convolutions vs. standard 2D conv in SD/SDXL/FLUX VAEs)
|
|
3. 16-dimensional latent space (z_dim=16)
|
|
"""
|
|
decoder_conv_in_key = "decoder.conv_in.weight"
|
|
if decoder_conv_in_key not in state_dict:
|
|
return False
|
|
weight = state_dict[decoder_conv_in_key]
|
|
shape = getattr(weight, "shape", None)
|
|
if shape is None or len(shape) != 5:
|
|
return False
|
|
# z_dim is the input channel dim of decoder.conv_in
|
|
return shape[1] == 16
|
|
|
|
|
|
def _is_flux2_vae(state_dict: dict[str | int, Any]) -> bool:
|
|
"""Check if state dict is a FLUX.2 VAE (AutoencoderKLFlux2).
|
|
|
|
FLUX.2 VAE can be identified by:
|
|
1. Batch Normalization layers (bn.running_mean, bn.running_var) - unique to FLUX.2
|
|
2. 32-dimensional latent space (decoder.conv_in has 32 input channels)
|
|
|
|
FLUX.1 VAE has 16-dimensional latent space and no BatchNorm layers.
|
|
"""
|
|
# Check for BN layer which is unique to FLUX.2 VAE
|
|
has_bn = "bn.running_mean" in state_dict or "bn.running_var" in state_dict
|
|
|
|
# Check for 32-channel latent space (FLUX.2 has 32, FLUX.1 has 16)
|
|
decoder_conv_in_key = "decoder.conv_in.weight"
|
|
has_32_latent_channels = decoder_conv_in_key in state_dict and state_dict[decoder_conv_in_key].shape[1] == 32
|
|
|
|
return has_bn or has_32_latent_channels
|
|
|
|
|
|
class VAE_Checkpoint_Config_Base(Checkpoint_Config_Base):
|
|
"""Model config for standalone VAE models."""
|
|
|
|
type: Literal[ModelType.VAE] = Field(default=ModelType.VAE)
|
|
format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint)
|
|
|
|
@classmethod
|
|
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
|
|
raise_if_not_file(mod)
|
|
|
|
raise_for_override_fields(cls, override_fields)
|
|
|
|
cls._validate_looks_like_vae(mod)
|
|
|
|
cls._validate_base(mod)
|
|
|
|
return cls(**override_fields)
|
|
|
|
@classmethod
|
|
def _validate_base(cls, mod: ModelOnDisk) -> None:
|
|
"""Raise `NotAMatch` if the model base does not match this config class."""
|
|
expected_base = cls.model_fields["base"].default
|
|
recognized_base = cls._get_base_or_raise(mod)
|
|
if expected_base is not recognized_base:
|
|
raise NotAMatchError(f"base is {recognized_base}, not {expected_base}")
|
|
|
|
@classmethod
|
|
def _validate_looks_like_vae(cls, mod: ModelOnDisk) -> None:
|
|
state_dict = mod.load_state_dict()
|
|
if not state_dict_has_any_keys_starting_with(
|
|
state_dict,
|
|
{
|
|
"encoder.conv_in",
|
|
"decoder.conv_in",
|
|
},
|
|
):
|
|
raise NotAMatchError("model does not match Checkpoint VAE heuristics")
|
|
|
|
# Exclude FLUX.2 VAEs - they have their own config class
|
|
if _is_flux2_vae(state_dict):
|
|
raise NotAMatchError("model is a FLUX.2 VAE, not a standard VAE")
|
|
|
|
# Exclude Qwen Image VAEs - they have their own config class
|
|
if _is_qwen_image_vae(state_dict):
|
|
raise NotAMatchError("model is a Qwen Image VAE, not a standard VAE")
|
|
|
|
@classmethod
|
|
def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType:
|
|
# First, try to identify by latent space dimensions (most reliable)
|
|
state_dict = mod.load_state_dict()
|
|
decoder_conv_in_key = "decoder.conv_in.weight"
|
|
if decoder_conv_in_key in state_dict:
|
|
latent_channels = state_dict[decoder_conv_in_key].shape[1]
|
|
if latent_channels == 16:
|
|
# Flux1 VAE has 16-dimensional latent space
|
|
return BaseModelType.Flux
|
|
elif latent_channels == 4:
|
|
# SD/SDXL VAE has 4-dimensional latent space
|
|
# Try to distinguish SD1/SD2/SDXL by name, fallback to SD1
|
|
for regexp, base in REGEX_TO_BASE.items():
|
|
if re.search(regexp, mod.path.name, re.IGNORECASE):
|
|
return base
|
|
# Default to SD1 if we can't determine from name
|
|
return BaseModelType.StableDiffusion1
|
|
|
|
# Fallback: guess based on name
|
|
for regexp, base in REGEX_TO_BASE.items():
|
|
if re.search(regexp, mod.path.name, re.IGNORECASE):
|
|
return base
|
|
|
|
raise NotAMatchError("cannot determine base type")
|
|
|
|
|
|
class VAE_Checkpoint_SD1_Config(VAE_Checkpoint_Config_Base, Config_Base):
|
|
base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1)
|
|
|
|
|
|
class VAE_Checkpoint_SD2_Config(VAE_Checkpoint_Config_Base, Config_Base):
|
|
base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2)
|
|
|
|
|
|
class VAE_Checkpoint_SDXL_Config(VAE_Checkpoint_Config_Base, Config_Base):
|
|
base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL)
|
|
|
|
|
|
class VAE_Checkpoint_FLUX_Config(VAE_Checkpoint_Config_Base, Config_Base):
|
|
base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux)
|
|
|
|
|
|
class VAE_Checkpoint_Flux2_Config(Checkpoint_Config_Base, Config_Base):
|
|
"""Model config for FLUX.2 VAE checkpoint models (AutoencoderKLFlux2)."""
|
|
|
|
type: Literal[ModelType.VAE] = Field(default=ModelType.VAE)
|
|
format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint)
|
|
base: Literal[BaseModelType.Flux2] = Field(default=BaseModelType.Flux2)
|
|
|
|
@classmethod
|
|
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
|
|
raise_if_not_file(mod)
|
|
|
|
raise_for_override_fields(cls, override_fields)
|
|
|
|
cls._validate_looks_like_vae(mod)
|
|
|
|
cls._validate_is_flux2_vae(mod)
|
|
|
|
return cls(**override_fields)
|
|
|
|
@classmethod
|
|
def _validate_looks_like_vae(cls, mod: ModelOnDisk) -> None:
|
|
if not state_dict_has_any_keys_starting_with(
|
|
mod.load_state_dict(),
|
|
{
|
|
"encoder.conv_in",
|
|
"decoder.conv_in",
|
|
},
|
|
):
|
|
raise NotAMatchError("model does not match Checkpoint VAE heuristics")
|
|
|
|
@classmethod
|
|
def _validate_is_flux2_vae(cls, mod: ModelOnDisk) -> None:
|
|
"""Validate that this is a FLUX.2 VAE, not FLUX.1."""
|
|
state_dict = mod.load_state_dict()
|
|
if not _is_flux2_vae(state_dict):
|
|
raise NotAMatchError("state dict does not look like a FLUX.2 VAE")
|
|
|
|
|
|
class VAE_Checkpoint_QwenImage_Config(Checkpoint_Config_Base, Config_Base):
|
|
"""Model config for Qwen Image VAE checkpoint models (AutoencoderKLQwenImage)."""
|
|
|
|
type: Literal[ModelType.VAE] = Field(default=ModelType.VAE)
|
|
format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint)
|
|
base: Literal[BaseModelType.QwenImage] = Field(default=BaseModelType.QwenImage)
|
|
|
|
@classmethod
|
|
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
|
|
raise_if_not_file(mod)
|
|
|
|
raise_for_override_fields(cls, override_fields)
|
|
|
|
state_dict = mod.load_state_dict()
|
|
if not _is_qwen_image_vae(state_dict):
|
|
raise NotAMatchError("state dict does not look like a Qwen Image VAE")
|
|
|
|
return cls(**override_fields)
|
|
|
|
|
|
def _has_anima_vae_keys(state_dict: dict[str | int, Any]) -> bool:
|
|
"""Check if state dict looks like an Anima QwenImage VAE (AutoencoderKLQwenImage).
|
|
|
|
The Anima VAE has a distinctive structure with:
|
|
- encoder.downsamples.* (instead of encoder.down_blocks)
|
|
- decoder.upsamples.* (instead of decoder.up_blocks)
|
|
- decoder.head.* / decoder.middle.*
|
|
- Top-level conv1/conv2 weights
|
|
"""
|
|
required_prefixes = {
|
|
"encoder.downsamples.",
|
|
"decoder.upsamples.",
|
|
"decoder.middle.",
|
|
}
|
|
return all(any(str(k).startswith(prefix) for k in state_dict) for prefix in required_prefixes)
|
|
|
|
|
|
class VAE_Checkpoint_Anima_Config(Checkpoint_Config_Base, Config_Base):
|
|
"""Model config for Anima QwenImage VAE checkpoint models (AutoencoderKLQwenImage)."""
|
|
|
|
type: Literal[ModelType.VAE] = Field(default=ModelType.VAE)
|
|
format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint)
|
|
base: Literal[BaseModelType.Anima] = Field(default=BaseModelType.Anima)
|
|
|
|
@classmethod
|
|
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
|
|
raise_if_not_file(mod)
|
|
|
|
raise_for_override_fields(cls, override_fields)
|
|
|
|
state_dict = mod.load_state_dict()
|
|
if not _has_anima_vae_keys(state_dict):
|
|
raise NotAMatchError("state dict does not look like an Anima QwenImage VAE")
|
|
|
|
return cls(**override_fields)
|
|
|
|
|
|
class VAE_Diffusers_Config_Base(Diffusers_Config_Base):
|
|
"""Model config for standalone VAE models (diffusers version)."""
|
|
|
|
type: Literal[ModelType.VAE] = Field(default=ModelType.VAE)
|
|
format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers)
|
|
|
|
@classmethod
|
|
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
|
|
raise_if_not_dir(mod)
|
|
|
|
raise_for_override_fields(cls, override_fields)
|
|
|
|
raise_for_class_name(
|
|
common_config_paths(mod.path),
|
|
{
|
|
"AutoencoderKL",
|
|
"AutoencoderTiny",
|
|
},
|
|
)
|
|
|
|
# Unfortunately it is difficult to distinguish SD1 and SDXL VAEs by config alone, so we may need to
|
|
# guess based on name if the config is inconclusive.
|
|
override_name = override_fields.get("name")
|
|
cls._validate_base(mod, override_name)
|
|
|
|
return cls(**override_fields)
|
|
|
|
@classmethod
|
|
def _validate_base(cls, mod: ModelOnDisk, override_name: str | None = None) -> None:
|
|
"""Raise `NotAMatch` if the model base does not match this config class."""
|
|
expected_base = cls.model_fields["base"].default
|
|
recognized_base = cls._get_base_or_raise(mod, override_name)
|
|
if expected_base is not recognized_base:
|
|
raise NotAMatchError(f"base is {recognized_base}, not {expected_base}")
|
|
|
|
@classmethod
|
|
def _config_looks_like_sdxl(cls, config: dict[str, Any]) -> bool:
|
|
# Heuristic: These config values that distinguish Stability's SD 1.x VAE from their SDXL VAE.
|
|
return config.get("scaling_factor", 0) == 0.13025 and config.get("sample_size") in [512, 1024]
|
|
|
|
@classmethod
|
|
def _name_looks_like_sdxl(cls, mod: ModelOnDisk, override_name: str | None = None) -> bool:
|
|
# Heuristic: SD and SDXL VAE are the same shape (3-channel RGB to 4-channel float scaled down
|
|
# by a factor of 8), so we can't necessarily tell them apart by config hyperparameters. Best
|
|
# we can do is guess based on name.
|
|
return bool(re.search(r"xl\b", override_name or mod.path.name, re.IGNORECASE))
|
|
|
|
@classmethod
|
|
def _get_base_or_raise(cls, mod: ModelOnDisk, override_name: str | None = None) -> BaseModelType:
|
|
config_dict = get_config_dict_or_raise(common_config_paths(mod.path))
|
|
if cls._config_looks_like_sdxl(config_dict):
|
|
return BaseModelType.StableDiffusionXL
|
|
elif cls._name_looks_like_sdxl(mod, override_name):
|
|
return BaseModelType.StableDiffusionXL
|
|
else:
|
|
# TODO(psyche): Figure out how to positively identify SD1 here, and raise if we can't. Until then, YOLO.
|
|
return BaseModelType.StableDiffusion1
|
|
|
|
|
|
class VAE_Diffusers_SD1_Config(VAE_Diffusers_Config_Base, Config_Base):
|
|
base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1)
|
|
|
|
|
|
class VAE_Diffusers_SDXL_Config(VAE_Diffusers_Config_Base, Config_Base):
|
|
base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL)
|
|
|
|
|
|
class VAE_Diffusers_Flux2_Config(Diffusers_Config_Base, Config_Base):
|
|
"""Model config for FLUX.2 VAE models in diffusers format (AutoencoderKLFlux2)."""
|
|
|
|
type: Literal[ModelType.VAE] = Field(default=ModelType.VAE)
|
|
format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers)
|
|
base: Literal[BaseModelType.Flux2] = Field(default=BaseModelType.Flux2)
|
|
|
|
@classmethod
|
|
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
|
|
raise_if_not_dir(mod)
|
|
|
|
raise_for_override_fields(cls, override_fields)
|
|
|
|
raise_for_class_name(
|
|
common_config_paths(mod.path),
|
|
{
|
|
"AutoencoderKLFlux2",
|
|
},
|
|
)
|
|
|
|
return cls(**override_fields)
|