Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,26 @@
# For more info see: https://www.aleksandrhovhannisyan.com/blog/crlf-vs-lf-normalizing-line-endings-in-git/
* text=auto
docker/** text eol=lf

# The frontend formatters read the file on disk, so a CRLF working tree makes `format:check` fail
# for every file -- including ones nobody touched. That leaves contributors on Windows unable to run
# the same check CI runs, which is how formatting errors reach CI in the first place.
#
# Only the working copy changes: these files are already stored with LF (`* text=auto` normalizes on
# commit), so nothing in history is rewritten.
invokeai/frontend/**/*.ts text eol=lf
invokeai/frontend/**/*.tsx text eol=lf
invokeai/frontend/**/*.mts text eol=lf
invokeai/frontend/**/*.js text eol=lf
invokeai/frontend/**/*.jsx text eol=lf
invokeai/frontend/**/*.mjs text eol=lf
invokeai/frontend/**/*.cjs text eol=lf
invokeai/frontend/**/*.json text eol=lf
invokeai/frontend/**/*.webmanifest text eol=lf
invokeai/frontend/**/*.css text eol=lf
invokeai/frontend/**/*.html text eol=lf
invokeai/frontend/**/*.md text eol=lf
invokeai/frontend/**/*.yaml text eol=lf
invokeai/frontend/**/*.yml text eol=lf
tests/test_model_probe/stripped_models/** filter=lfs diff=lfs merge=lfs -text
tests/model_identification/stripped_models/** filter=lfs diff=lfs merge=lfs -text
9 changes: 9 additions & 0 deletions invokeai/app/invocations/anima/anima_model_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ class AnimaModelLoaderInvocation(BaseInvocation):
description="Standalone VAE model. Anima uses a Wan 2.1 / QwenImage VAE (16-channel). "
"A FLUX VAE can also be used as a compatible fallback.",
input=Input.Direct,
# `anima_l2i` accepts AutoencoderKLWan or FluxAutoEncoder. The Wan-family file is the same
# checkpoint whichever of the three bases it was installed under; declaring nothing here
# offered every VAE ever installed, including ones that cannot decode an Anima latent.
ui_model_base=[
BaseModelType.Anima,
BaseModelType.QwenImage,
BaseModelType.Wan,
BaseModelType.Flux,
],
ui_model_type=ModelType.VAE,
title="VAE",
)
Expand Down
7 changes: 5 additions & 2 deletions invokeai/app/invocations/flux2/flux2_klein_model_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,13 @@ class Flux2KleinModelLoaderInvocation(BaseInvocation):

vae_model: Optional[ModelIdentifierField] = InputField(
default=None,
description="Standalone VAE model. Flux2 Klein uses the same VAE as FLUX (16-channel). "
description="Standalone VAE model (AutoencoderKLFlux2, 32-channel). "
"If not provided, VAE will be loaded from the Qwen3 Source model.",
input=Input.Direct,
ui_model_base=[BaseModelType.Flux, BaseModelType.Flux2],
# A FLUX VAE was offered here and does not work: it is the legacy `AutoEncoder` class with
# a 16-channel latent space, and decoding a FLUX.2 latent with it raises
# `AutoEncoder.decode() got an unexpected keyword argument 'return_dict'`.
ui_model_base=BaseModelType.Flux2,
ui_model_type=ModelType.VAE,
title="VAE",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ class QwenImageModelLoaderInvocation(BaseInvocation):
description="Standalone Qwen Image VAE model. "
"If not provided, VAE will be loaded from the Component Source (or from the main model if it is Diffusers).",
input=Input.Direct,
ui_model_base=BaseModelType.QwenImage,
# The same 16-channel checkpoint is registered under either base depending on which
# family it was installed for; krea2_model_loader already offers both.
ui_model_base=[BaseModelType.QwenImage, BaseModelType.Anima],
ui_model_type=ModelType.VAE,
title="VAE",
)
Expand Down
14 changes: 14 additions & 0 deletions invokeai/backend/architectures/defs/anima.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from invokeai.backend.architectures.facets.features import FeaturesFacet, NegativePrompt
from invokeai.backend.architectures.facets.latent_space import WAN21_16, LatentSpaceFacet
from invokeai.backend.architectures.facets.modality import ModalityFacet
from invokeai.backend.architectures.facets.vae import VaeCompatibility, VaeFacet
from invokeai.backend.architectures.registry import register
from invokeai.backend.model_manager.configs.default_settings import MainModelDefaultSettings
from invokeai.backend.model_manager.taxonomy import BaseModelType
Expand All @@ -26,4 +27,17 @@
scheduler_set="anima",
scheduler_applies_to_graph=True,
),
VaeFacet(
frozenset(
{
# `anima_l2i` accepts AutoencoderKLWan or FluxAutoEncoder, each with its own
# decode path. The Wan-family file is registered under whichever base it was
# installed for -- all three point at the same 194-tensor checkpoint.
VaeCompatibility(BaseModelType.Anima),
VaeCompatibility(BaseModelType.QwenImage),
VaeCompatibility(BaseModelType.Wan, latent_channels=16),
VaeCompatibility(BaseModelType.Flux),
}
)
),
)
5 changes: 5 additions & 0 deletions invokeai/backend/architectures/defs/ernie_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,10 @@
dimension_grid=16,
guidance_label="CFG",
scheduler_set="flow",
# `ernie_image_denoise` takes a `scheduler` field and builds the sampler from it
# (ERNIE_IMAGE_SCHEDULER_MAP), so the choice reaches the graph rather than being a
# UI affordance. Omitting this defaulted it to False, which would have hidden the
# dropdown and pinned every generation to the first entry.
scheduler_applies_to_graph=True,
),
)
11 changes: 11 additions & 0 deletions invokeai/backend/architectures/defs/krea_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from invokeai.backend.architectures.facets.features import FeaturesFacet, NegativePrompt
from invokeai.backend.architectures.facets.latent_space import WAN21_16, LatentSpaceFacet
from invokeai.backend.architectures.facets.modality import ModalityFacet
from invokeai.backend.architectures.facets.vae import VaeCompatibility, VaeFacet
from invokeai.backend.architectures.registry import register
from invokeai.backend.model_manager.configs.default_settings import MainModelDefaultSettings
from invokeai.backend.model_manager.taxonomy import BaseModelType, Krea2VariantType
Expand Down Expand Up @@ -34,4 +35,14 @@
scheduler_set="flow",
supports_regional_guidance=True,
),
VaeFacet(
frozenset(
{
# Krea-2 decodes with the Qwen-Image VAE, which is why its graph reuses
# `qwen_image_l2i`. The same file also appears registered as `anima`.
VaeCompatibility(BaseModelType.QwenImage),
VaeCompatibility(BaseModelType.Anima),
}
)
),
)
9 changes: 9 additions & 0 deletions invokeai/backend/architectures/defs/qwen_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from invokeai.backend.architectures.facets.features import FeaturesFacet, NegativePrompt
from invokeai.backend.architectures.facets.latent_space import WAN21_16, LatentSpaceFacet
from invokeai.backend.architectures.facets.modality import ModalityFacet
from invokeai.backend.architectures.facets.vae import VaeCompatibility, VaeFacet
from invokeai.backend.architectures.registry import register
from invokeai.backend.model_manager.configs.default_settings import MainModelDefaultSettings
from invokeai.backend.model_manager.taxonomy import BaseModelType
Expand All @@ -27,4 +28,12 @@
max_reference_images=5,
reference_images_require_variant="edit",
),
VaeFacet(
frozenset(
{
VaeCompatibility(BaseModelType.QwenImage),
VaeCompatibility(BaseModelType.Anima),
}
)
),
)
12 changes: 12 additions & 0 deletions invokeai/backend/architectures/defs/wan.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from invokeai.backend.architectures.facets.features import FeaturesFacet, NegativePrompt
from invokeai.backend.architectures.facets.latent_space import WAN21_16, WAN22_48, LatentSpaceFacet
from invokeai.backend.architectures.facets.modality import ModalityFacet
from invokeai.backend.architectures.facets.vae import VaeCompatibility, VaeFacet
from invokeai.backend.architectures.registry import register
from invokeai.backend.model_manager.configs.default_settings import MainModelDefaultSettings
from invokeai.backend.model_manager.taxonomy import BaseModelType, WanVariantType
Expand Down Expand Up @@ -38,4 +39,15 @@
guidance_label="Guidance",
scheduler_set="flow",
),
VaeFacet(
frozenset(
{
# A14B uses the 16-channel Wan 2.1 VAE; TI2V-5B needs the 48-channel
# Wan2.2-VAE. Both are registered under `wan`, so the channel count is the
# only thing that tells them apart.
VaeCompatibility(BaseModelType.Wan, latent_channels=16),
VaeCompatibility(BaseModelType.Wan, latent_channels=48),
}
)
),
)
9 changes: 9 additions & 0 deletions invokeai/backend/architectures/defs/z_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from invokeai.backend.architectures.facets.features import FeaturesFacet, NegativePrompt
from invokeai.backend.architectures.facets.latent_space import FLUX_16, LatentSpaceFacet
from invokeai.backend.architectures.facets.modality import ModalityFacet
from invokeai.backend.architectures.facets.vae import VaeCompatibility, VaeFacet
from invokeai.backend.architectures.registry import register
from invokeai.backend.model_manager.configs.default_settings import MainModelDefaultSettings
from invokeai.backend.model_manager.taxonomy import BaseModelType, ZImageVariantType
Expand Down Expand Up @@ -34,4 +35,12 @@
scheduler_applies_to_graph=True,
control_kinds=frozenset({"z_image_control"}),
),
VaeFacet(
frozenset(
{
# Z-Image decodes with a FLUX-compatible VAE; `z_image_model_loader` says so.
VaeCompatibility(BaseModelType.Flux),
}
)
),
)
78 changes: 78 additions & 0 deletions invokeai/backend/architectures/facets/vae.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Which VAEs an architecture can decode with.

Not derivable from the latent geometry. Eight architectures share a 16-channel space at 8x
compression, but their VAEs are not interchangeable: SD 3.5 and CogView 4 are 16-channel too and
belong to neither the Wan/Qwen family nor FLUX's. Compatibility is a fact about the decoder class,
so it is declared.

Nor is it derivable from the VAE's `base` alone. The same physical file is registered under
`anima` or `qwen-image` depending on which family it was installed for -- byte-identical, 194
tensors -- and a `wan` VAE may be either the 16-channel Wan 2.1 file (the same family again) or
TI2V-5B's 48-channel Wan2.2-VAE, which fits nothing else. `VAE_Checkpoint_Wan_Config` already
records `latent_channels`; this facet is what finally reads it.
"""

from dataclasses import dataclass
from typing import ClassVar

from invokeai.backend.architectures.facet import Facet
from invokeai.backend.architectures.registry import get
from invokeai.backend.model_manager.taxonomy import BaseModelType


@dataclass(frozen=True)
class VaeCompatibility:
"""One VAE an architecture accepts, as a model record can be matched against it."""

base: BaseModelType

latent_channels: int | None = None
"""Required channel count, where the base alone is ambiguous.

Only `wan` is: its VAEs carry `latent_channels` of 16 or 48 and only the 16-channel ones belong
to the shared family. `None` means the base is unambiguous and the field is not consulted --
which is also what happens for VAE configs that do not carry it.
"""

def matches(self, vae_base: BaseModelType, vae_latent_channels: int | None = None) -> bool:
if vae_base != self.base:
return False
if self.latent_channels is None:
return True
return vae_latent_channels == self.latent_channels


@dataclass(frozen=True)
class VaeFacet(Facet):
"""The VAEs a model of this architecture can be paired with.

Includes the architecture's own base wherever that is accepted, so the set is complete on its
own and a consumer never has to add an implicit "or its own".
"""

REQUIRED: ClassVar[bool] = False

accepted: frozenset[VaeCompatibility]

def accepts(self, vae_base: BaseModelType, vae_latent_channels: int | None = None) -> bool:
return any(entry.matches(vae_base, vae_latent_channels) for entry in self.accepted)

@property
def accepted_bases(self) -> frozenset[BaseModelType]:
"""The bases alone, for the UI's first-pass filter and for comparing against a loader's
`ui_model_base`, which cannot express a channel constraint."""
return frozenset(entry.base for entry in self.accepted)


def accepts_vae(base: BaseModelType, vae_base: BaseModelType, vae_latent_channels: int | None = None) -> bool:
"""Whether `base` can decode with this VAE.

Architectures that declare no `VaeFacet` accept only their own base -- the SD family and
anything whose loader has no VAE input at all.
"""
facet = get(base, VaeFacet)

if facet is None:
return vae_base == base

return facet.accepts(vae_base, vae_latent_channels)
24 changes: 19 additions & 5 deletions invokeai/backend/model_manager/load/model_loaders/vae.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,18 +279,32 @@ def _load_wan_vae_diffusers(self, config: VAE_Diffusers_Wan_Config) -> AnyModel:
def _load_qwen_image_vae(self, config: VAE_Checkpoint_QwenImage_Config) -> AnyModel:
"""Load a Qwen Image VAE from a single safetensors file.

The Qwen Image VAE checkpoint is expected to be in the diffusers state-dict
layout (i.e. the same keys as `vae/diffusion_pytorch_model.safetensors` from
the Qwen-Image repo). `AutoencoderKLQwenImage` does not register a single-file
conversion in diffusers, so we instantiate the model with default config and
load the state dict directly.
Two layouts reach this method. Files exported from the Qwen-Image repo carry the diffusers
state-dict keys (`decoder.conv_in.weight`, ...) and are loaded directly, because
`AutoencoderKLQwenImage` registers no single-file conversion in diffusers.

Community redistributions carry the original layout instead (`decoder.conv1.weight`, ...),
which needs converting. Those files are the 16-channel Wan-family VAE -- architecturally the
same autoencoder -- so `AutoencoderKLWan.from_single_file` reads them, and the identical
checkpoint installed under `anima` already takes that path. Loading them into
`AutoencoderKLQwenImage` with `strict=True` failed with 194 missing keys, which made a VAE
unusable purely because of the base it happened to be probed as.
"""
import accelerate
from diffusers.models.autoencoders.autoencoder_kl_qwenimage import AutoencoderKLQwenImage
from safetensors.torch import load_file

sd = load_file(config.path)

if "decoder.conv_in.weight" not in sd:
from diffusers.models.autoencoders import AutoencoderKLWan

from invokeai.backend.wan.rocm_causal_conv3d import patch_wan_causal_conv3d_for_rocm

del sd
patch_wan_causal_conv3d_for_rocm()
return AutoencoderKLWan.from_single_file(config.path, torch_dtype=self._torch_dtype)

if self._torch_dtype is not None:
for k in list(sd.keys()):
if sd[k].is_floating_point():
Expand Down
7 changes: 4 additions & 3 deletions invokeai/frontend/web/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -15564,6 +15564,7 @@
"input": "direct",
"orig_required": true,
"title": "VAE",
"ui_model_base": ["anima", "qwen-image", "wan", "flux"],
"ui_model_type": ["vae"]
},
"qwen3_encoder_model": {
Expand Down Expand Up @@ -31794,13 +31795,13 @@
}
],
"default": null,
"description": "Standalone VAE model. Flux2 Klein uses the same VAE as FLUX (16-channel). If not provided, VAE will be loaded from the Qwen3 Source model.",
"description": "Standalone VAE model (AutoencoderKLFlux2, 32-channel). If not provided, VAE will be loaded from the Qwen3 Source model.",
"field_kind": "input",
"input": "direct",
"orig_default": null,
"orig_required": false,
"title": "VAE",
"ui_model_base": ["flux", "flux2"],
"ui_model_base": ["flux2"],
"ui_model_type": ["vae"]
},
"qwen3_encoder_model": {
Expand Down Expand Up @@ -80267,7 +80268,7 @@
"orig_default": null,
"orig_required": false,
"title": "VAE",
"ui_model_base": ["qwen-image"],
"ui_model_base": ["qwen-image", "anima"],
"ui_model_type": ["vae"]
},
"qwen_vl_encoder_model": {
Expand Down
2 changes: 1 addition & 1 deletion invokeai/frontend/web/src/services/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12823,7 +12823,7 @@ export type components = {
model: components["schemas"]["ModelIdentifierField"];
/**
* VAE
* @description Standalone VAE model. Flux2 Klein uses the same VAE as FLUX (16-channel). If not provided, VAE will be loaded from the Qwen3 Source model.
* @description Standalone VAE model (AutoencoderKLFlux2, 32-channel). If not provided, VAE will be loaded from the Qwen3 Source model.
* @default null
*/
vae_model?: components["schemas"]["ModelIdentifierField"] | null;
Expand Down
Loading
Loading