chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:22:06 +08:00
commit cddb07a176
3370 changed files with 685519 additions and 0 deletions
View File
@@ -0,0 +1,764 @@
"""Tests for the Anima ControlNet-LLLite adapter — construction from a saved
state dict, exact-passthrough guarantees, forward-swap binding/restore,
multi-adapter composition, and the conditioning image preprocessing helpers."""
import os
from pathlib import Path
import pytest
import torch
import torch.nn.functional as F
from torch import nn
from invokeai.app.invocations.anima_denoise import AnimaDenoiseInvocation
from invokeai.app.invocations.anima_lllite import AnimaLLLiteField
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.backend.anima.control_net_lllite import (
AnimaControlNetLLLite,
build_inpaint_cond_image,
prepare_cond_image,
prepare_mask,
target_cond_hw,
)
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
# Opt-in test against the real adapter weights (anima-lllite-inpainting-v2.safetensors).
_REAL_WEIGHTS_ENV_VAR = "ANIMA_LLLITE_WEIGHTS_PATH"
REAL_WEIGHTS_PATH = Path(os.environ[_REAL_WEIGHTS_ENV_VAR]) if _REAL_WEIGHTS_ENV_VAR in os.environ else None
COND_DIM = 16
COND_EMB_DIM = 8
MLP_DIM = 8
IN_DIM = 16
N_BLOCKS = 2
KINDS = ("self_attn_q_proj", "self_attn_k_proj", "self_attn_v_proj", "mlp_layer1")
def make_synthetic_state_dict(seed: int = 0, mlp_dim: int = MLP_DIM) -> dict[str, torch.Tensor]:
"""Saved-format (v2 named-key) state dict for a tiny 2-block adapter with
4-channel (inpaint) conditioning and one trunk resblock."""
g = torch.Generator().manual_seed(seed)
def t(*shape: int) -> torch.Tensor:
return torch.randn(*shape, generator=g)
ch_half = COND_DIM // 2
sd = {
"lllite_conditioning1.conv1.weight": t(ch_half, 4, 4, 4),
"lllite_conditioning1.conv1.bias": t(ch_half),
"lllite_conditioning1.norm1.weight": t(ch_half),
"lllite_conditioning1.norm1.bias": t(ch_half),
"lllite_conditioning1.conv2.weight": t(ch_half, ch_half, 3, 3),
"lllite_conditioning1.conv2.bias": t(ch_half),
"lllite_conditioning1.norm2.weight": t(ch_half),
"lllite_conditioning1.norm2.bias": t(ch_half),
"lllite_conditioning1.conv3.weight": t(COND_DIM, ch_half, 4, 4),
"lllite_conditioning1.conv3.bias": t(COND_DIM),
"lllite_conditioning1.norm3.weight": t(COND_DIM),
"lllite_conditioning1.norm3.bias": t(COND_DIM),
"lllite_conditioning1.resblocks.0.norm1.weight": t(COND_DIM),
"lllite_conditioning1.resblocks.0.norm1.bias": t(COND_DIM),
"lllite_conditioning1.resblocks.0.conv1.weight": t(COND_DIM, COND_DIM, 3, 3),
"lllite_conditioning1.resblocks.0.conv1.bias": t(COND_DIM),
"lllite_conditioning1.resblocks.0.norm2.weight": t(COND_DIM),
"lllite_conditioning1.resblocks.0.norm2.bias": t(COND_DIM),
"lllite_conditioning1.resblocks.0.conv2.weight": t(COND_DIM, COND_DIM, 3, 3),
"lllite_conditioning1.resblocks.0.conv2.bias": t(COND_DIM),
"lllite_conditioning1.proj.weight": t(COND_EMB_DIM, COND_DIM, 1, 1),
"lllite_conditioning1.proj.bias": t(COND_EMB_DIM),
"lllite_conditioning1.out_norm.weight": t(COND_EMB_DIM),
"lllite_conditioning1.out_norm.bias": t(COND_EMB_DIM),
}
for i in range(N_BLOCKS):
for kind in KINDS:
p = f"lllite_dit_blocks_{i}_{kind}"
sd[f"{p}.down.weight"] = t(mlp_dim, IN_DIM)
sd[f"{p}.down.bias"] = t(mlp_dim)
sd[f"{p}.mid.weight"] = t(mlp_dim, mlp_dim + COND_EMB_DIM)
sd[f"{p}.mid.bias"] = t(mlp_dim)
sd[f"{p}.cond_to_film.weight"] = t(2 * mlp_dim, COND_EMB_DIM)
sd[f"{p}.cond_to_film.bias"] = t(2 * mlp_dim)
sd[f"{p}.up.weight"] = t(IN_DIM, mlp_dim)
sd[f"{p}.up.bias"] = t(IN_DIM)
sd[f"{p}.depth_embed"] = t(COND_EMB_DIM)
return sd
class FakeAttention(nn.Module):
def __init__(self, dim: int):
super().__init__()
self.q_proj = nn.Linear(dim, dim, bias=False)
self.k_proj = nn.Linear(dim, dim, bias=False)
self.v_proj = nn.Linear(dim, dim, bias=False)
class FakeMlp(nn.Module):
def __init__(self, dim: int):
super().__init__()
self.layer1 = nn.Linear(dim, dim * 2, bias=False)
class FakeBlock(nn.Module):
def __init__(self, dim: int):
super().__init__()
self.self_attn = FakeAttention(dim)
self.cross_attn = FakeAttention(dim)
self.mlp = FakeMlp(dim)
class FakeTransformer(nn.Module):
def __init__(self, dim: int, n_blocks: int):
super().__init__()
self.blocks = nn.ModuleList([FakeBlock(dim) for _ in range(n_blocks)])
def make_model_and_transformer(dim: int = IN_DIM) -> tuple[AnimaControlNetLLLite, FakeTransformer]:
model = AnimaControlNetLLLite.from_state_dict(make_synthetic_state_dict(), None)
torch.manual_seed(123)
transformer = FakeTransformer(dim, N_BLOCKS)
return model, transformer
def matching_cond_image() -> torch.Tensor:
"""4ch cond image sized for latent 4x4 -> trunk tokens S = 2*2 = 4."""
return torch.randn(1, 4, 32, 32, generator=torch.Generator().manual_seed(7)).clamp(-1, 1)
def plain_linear(linear: nn.Linear, x: torch.Tensor) -> torch.Tensor:
return F.linear(x, linear.weight, linear.bias)
# ----------------------------------------------------------------------------
# Preprocessing helpers
# ----------------------------------------------------------------------------
def test_target_cond_hw_even_latent() -> None:
assert target_cond_hw(128, 128) == (1024, 1024)
assert target_cond_hw(64, 96) == (512, 768)
def test_target_cond_hw_odd_latent_pads_to_patch_multiple() -> None:
assert target_cond_hw(129, 129) == (1040, 1040)
assert target_cond_hw(129, 64) == (1040, 512)
assert target_cond_hw(5, 5, patch_spatial=4) == (64, 64)
def test_prepare_cond_image_no_resize_is_exact_rescale() -> None:
rgb = torch.rand(1, 3, 16, 16, generator=torch.Generator().manual_seed(0))
out = prepare_cond_image(rgb, latent_h=2, latent_w=2)
assert torch.equal(out, rgb * 2.0 - 1.0)
def test_prepare_cond_image_resizes_takes_first_frame_and_stays_in_range() -> None:
rgb = torch.rand(2, 3, 100, 100, generator=torch.Generator().manual_seed(1))
out = prepare_cond_image(rgb, latent_h=9, latent_w=8)
assert out.shape == (1, 3, 80, 64)
assert out.min().item() >= -1.0
assert out.max().item() <= 1.0
def test_prepare_cond_image_rejects_bad_shape() -> None:
with pytest.raises(ValueError, match="Unexpected cond image shape"):
prepare_cond_image(torch.rand(1, 4, 16, 16), latent_h=2, latent_w=2)
def test_prepare_mask_binarizes_at_half() -> None:
mask = torch.full((1, 1, 16, 16), 0.49)
mask[:, :, 8:, :] = 0.5
out = prepare_mask(mask, latent_h=2, latent_w=2)
assert torch.equal(out[:, :, :8, :], torch.zeros(1, 1, 8, 16))
assert torch.equal(out[:, :, 8:, :], torch.ones(1, 1, 8, 16))
def test_prepare_mask_accepts_3d_and_resizes_nearest() -> None:
mask = torch.zeros(1, 8, 8)
mask[:, :4, :] = 1.0
out = prepare_mask(mask, latent_h=4, latent_w=4)
assert out.shape == (1, 1, 32, 32)
assert set(out.unique().tolist()) <= {0.0, 1.0}
assert torch.equal(out[:, :, :16, :], torch.ones(1, 1, 16, 32))
assert torch.equal(out[:, :, 16:, :], torch.zeros(1, 1, 16, 32))
def test_prepare_mask_rejects_bad_shape() -> None:
with pytest.raises(ValueError, match="Unexpected mask shape"):
prepare_mask(torch.rand(1, 3, 16, 16), latent_h=2, latent_w=2)
def test_build_inpaint_cond_image_polarity_and_range() -> None:
rgb = torch.full((1, 3, 4, 4), 0.5)
mask = torch.zeros(1, 1, 4, 4)
mask[:, :, :2, :] = 1.0 # white = inpaint area
out = build_inpaint_cond_image(rgb, mask, masked_input=True)
assert out.shape == (1, 4, 4, 4)
# RGB zeroed under the inpaint area, untouched elsewhere.
assert torch.equal(out[:, :3, :2, :], torch.zeros(1, 3, 2, 4))
assert torch.equal(out[:, :3, 2:, :], rgb[:, :, 2:, :])
# Mask channel rescaled to [-1, 1]: +1 = inpaint, -1 = keep.
assert torch.equal(out[:, 3:, :2, :], torch.ones(1, 1, 2, 4))
assert torch.equal(out[:, 3:, 2:, :], -torch.ones(1, 1, 2, 4))
def test_build_inpaint_cond_image_without_masked_input_keeps_rgb() -> None:
rgb = torch.full((1, 3, 4, 4), 0.5)
mask = torch.ones(1, 1, 4, 4)
out = build_inpaint_cond_image(rgb, mask, masked_input=False)
assert torch.equal(out[:, :3], rgb)
assert torch.equal(out[:, 3:], torch.ones(1, 1, 4, 4))
# ----------------------------------------------------------------------------
# Construction from state dict
# ----------------------------------------------------------------------------
def test_from_state_dict_synthetic_shape_fallbacks() -> None:
sd = make_synthetic_state_dict()
model = AnimaControlNetLLLite.from_state_dict(sd, None)
assert len(model.lllite_modules) == N_BLOCKS * len(KINDS)
assert model.cond_in_channels == 4
assert model.cond_emb_dim == COND_EMB_DIM
assert model.mlp_dim == MLP_DIM
assert model.cond_dim == COND_DIM
assert model.cond_resblocks == 1
assert model.use_aspp is False
assert model.inpaint_masked_input is False # metadata-only, defaults False
# Weights actually landed where they belong.
assert torch.equal(model.conditioning1.conv1.weight, sd["lllite_conditioning1.conv1.weight"])
by_name = {m.lllite_name: m for m in model.lllite_modules}
m0 = by_name["lllite_dit_blocks_0_self_attn_q_proj"]
assert torch.equal(m0.depth_embed, sd["lllite_dit_blocks_0_self_attn_q_proj.depth_embed"])
assert torch.equal(m0.up.weight, sd["lllite_dit_blocks_0_self_attn_q_proj.up.weight"])
m1 = by_name["lllite_dit_blocks_1_mlp_layer1"]
assert torch.equal(m1.down.weight, sd["lllite_dit_blocks_1_mlp_layer1.down.weight"])
assert not any(p.requires_grad for p in model.parameters())
assert not model.training
def test_from_state_dict_metadata_wins() -> None:
metadata = {
"lllite.cond_emb_dim": str(COND_EMB_DIM),
"lllite.mlp_dim": str(MLP_DIM),
"lllite.cond_dim": str(COND_DIM),
"lllite.cond_resblocks": "1",
"lllite.use_aspp": "false",
"lllite.cond_in_channels": "4",
"lllite.inpaint_masked_input": "true",
}
model = AnimaControlNetLLLite.from_state_dict(make_synthetic_state_dict(), metadata)
assert model.inpaint_masked_input is True
assert model.cond_in_channels == 4
def test_from_state_dict_rejects_legacy_format() -> None:
sd = make_synthetic_state_dict()
sd["lllite_modules.0.down.weight"] = torch.zeros(MLP_DIM, IN_DIM)
with pytest.raises(ValueError, match="legacy"):
AnimaControlNetLLLite.from_state_dict(sd, None)
def test_from_state_dict_strict_on_unknown_keys() -> None:
sd = make_synthetic_state_dict()
sd["some_unrelated_key"] = torch.zeros(1)
with pytest.raises(RuntimeError, match="some_unrelated_key"):
AnimaControlNetLLLite.from_state_dict(sd, None)
sd = make_synthetic_state_dict()
sd["lllite_dit_blocks_0_self_attn_q_proj.extra.weight"] = torch.zeros(1)
with pytest.raises(RuntimeError, match="extra"):
AnimaControlNetLLLite.from_state_dict(sd, None)
def test_from_state_dict_strict_on_missing_keys() -> None:
sd = make_synthetic_state_dict()
del sd["lllite_dit_blocks_0_self_attn_q_proj.depth_embed"]
with pytest.raises(RuntimeError, match="depth_embed"):
AnimaControlNetLLLite.from_state_dict(sd, None)
def test_from_state_dict_requires_modules() -> None:
sd = {k: v for k, v in make_synthetic_state_dict().items() if k.startswith("lllite_conditioning1.")}
with pytest.raises(ValueError, match="no LLLite modules"):
AnimaControlNetLLLite.from_state_dict(sd, None)
def test_from_state_dict_missing_down_weight_raises_value_error() -> None:
sd = make_synthetic_state_dict()
del sd["lllite_dit_blocks_0_self_attn_q_proj.down.weight"]
with pytest.raises(ValueError, match="missing key 'lllite_dit_blocks_0_self_attn_q_proj.down.weight'"):
AnimaControlNetLLLite.from_state_dict(sd, None)
# ----------------------------------------------------------------------------
# Binding / restore
# ----------------------------------------------------------------------------
def test_apply_to_swaps_forward_and_restore_is_bit_exact() -> None:
model, transformer = make_model_and_transformer()
x = torch.randn(1, 4, IN_DIM, generator=torch.Generator().manual_seed(2))
q_proj = transformer.blocks[0].self_attn.q_proj
expected = plain_linear(q_proj, x)
model.apply_to(transformer)
model.set_multiplier(1.0)
model.set_cond_image(matching_cond_image())
assert not torch.equal(q_proj(x), expected)
model.restore()
assert torch.equal(q_proj(x), expected)
def test_apply_to_is_idempotent() -> None:
model, transformer = make_model_and_transformer()
x = torch.randn(1, 4, IN_DIM, generator=torch.Generator().manual_seed(3))
v_proj = transformer.blocks[1].self_attn.v_proj
expected = plain_linear(v_proj, x)
model.apply_to(transformer)
model.apply_to(transformer) # must not double-wrap
model.restore()
assert torch.equal(v_proj(x), expected)
def test_restore_without_apply_is_safe() -> None:
model, _ = make_model_and_transformer()
model.restore()
model.restore()
def test_restore_does_not_pin_instance_level_forward() -> None:
# An instance-level `forward` left behind after restore() would silently
# bypass class-level forward swaps that share the module __dict__ (see
# wrap_custom_layer notes in model_manager/load/load_default.py).
model, transformer = make_model_and_transformer()
q_proj = transformer.blocks[0].self_attn.q_proj
assert "forward" not in q_proj.__dict__
model.apply_to(transformer)
assert "forward" in q_proj.__dict__
model.restore()
assert "forward" not in q_proj.__dict__
def test_restore_preserves_preexisting_instance_level_forward() -> None:
model, transformer = make_model_and_transformer()
q_proj = transformer.blocks[0].self_attn.q_proj
sentinel = q_proj.forward
q_proj.forward = sentinel # pre-existing instance-level forward
model.apply_to(transformer)
model.restore()
assert q_proj.__dict__.get("forward") is sentinel
def test_apply_to_rejects_in_features_mismatch() -> None:
model, _ = make_model_and_transformer()
transformer = FakeTransformer(IN_DIM * 2, N_BLOCKS)
with pytest.raises(ValueError, match="in_features"):
model.apply_to(transformer)
def test_apply_to_rejects_missing_block() -> None:
model, _ = make_model_and_transformer()
transformer = FakeTransformer(IN_DIM, 1)
with pytest.raises(ValueError, match="block"):
model.apply_to(transformer)
# ----------------------------------------------------------------------------
# Forward: passthrough guarantees and active path
# ----------------------------------------------------------------------------
def test_passthrough_multiplier_zero_is_bit_exact() -> None:
model, transformer = make_model_and_transformer()
model.apply_to(transformer)
model.set_cond_image(matching_cond_image())
model.set_multiplier(0.0)
x = torch.randn(1, 4, IN_DIM, generator=torch.Generator().manual_seed(4))
for block in transformer.blocks:
for linear in (block.self_attn.q_proj, block.self_attn.k_proj, block.self_attn.v_proj):
assert torch.equal(linear(x), plain_linear(linear, x))
def test_passthrough_no_cond_is_bit_exact() -> None:
model, transformer = make_model_and_transformer()
model.apply_to(transformer)
model.set_multiplier(1.0)
q_proj = transformer.blocks[0].self_attn.q_proj
x = torch.randn(1, 4, IN_DIM, generator=torch.Generator().manual_seed(5))
assert torch.equal(q_proj(x), plain_linear(q_proj, x))
model.set_cond_image(matching_cond_image())
assert not torch.equal(q_proj(x), plain_linear(q_proj, x))
model.set_cond_image(None) # clearing re-enables passthrough
assert torch.equal(q_proj(x), plain_linear(q_proj, x))
def test_passthrough_on_seq_len_mismatch_is_bit_exact() -> None:
model, transformer = make_model_and_transformer()
model.apply_to(transformer)
model.set_multiplier(1.0)
# Cond image for latent 6x6 -> S = 3*3 = 9, but x has S = 4.
model.set_cond_image(torch.randn(1, 4, 48, 48, generator=torch.Generator().manual_seed(6)))
q_proj = transformer.blocks[0].self_attn.q_proj
x = torch.randn(1, 4, IN_DIM, generator=torch.Generator().manual_seed(7))
assert torch.equal(q_proj(x), plain_linear(q_proj, x))
def test_passthrough_on_non_divisible_batch_is_bit_exact() -> None:
model, transformer = make_model_and_transformer()
model.apply_to(transformer)
model.set_multiplier(1.0)
model.set_cond_image(matching_cond_image().repeat(2, 1, 1, 1)) # cond batch 2
q_proj = transformer.blocks[0].self_attn.q_proj
x = torch.randn(3, 4, IN_DIM, generator=torch.Generator().manual_seed(8)) # 3 % 2 != 0
assert torch.equal(q_proj(x), plain_linear(q_proj, x))
def test_cfg_batch_broadcast_matches_single_sample() -> None:
model, transformer = make_model_and_transformer()
model.apply_to(transformer)
model.set_multiplier(1.0)
model.set_cond_image(matching_cond_image()) # cond batch 1
q_proj = transformer.blocks[0].self_attn.q_proj
xa = torch.randn(1, 4, IN_DIM, generator=torch.Generator().manual_seed(9))
xb = torch.randn(1, 4, IN_DIM, generator=torch.Generator().manual_seed(10))
y_batched = q_proj(torch.cat([xa, xb], dim=0))
assert not torch.equal(y_batched, plain_linear(q_proj, torch.cat([xa, xb], dim=0)))
assert torch.allclose(y_batched[0:1], q_proj(xa), rtol=1e-5, atol=1e-6)
assert torch.allclose(y_batched[1:2], q_proj(xb), rtol=1e-5, atol=1e-6)
def test_5d_mlp_input_matches_flattened_3d_path() -> None:
model, transformer = make_model_and_transformer()
model.apply_to(transformer)
model.set_multiplier(1.0)
model.set_cond_image(matching_cond_image())
layer1 = transformer.blocks[0].mlp.layer1
x5 = torch.randn(1, 1, 2, 2, IN_DIM, generator=torch.Generator().manual_seed(11))
y5 = layer1(x5)
assert y5.shape == (1, 1, 2, 2, IN_DIM * 2)
y3 = layer1(x5.reshape(1, 4, IN_DIM))
assert torch.equal(y5, y3.reshape(1, 1, 2, 2, -1))
assert not torch.equal(y5, plain_linear(layer1, x5))
def test_5d_passthrough_keeps_shape() -> None:
model, transformer = make_model_and_transformer()
model.apply_to(transformer)
model.set_multiplier(1.0)
# Seq mismatch: trunk S = 9, x flattened S = 4 -> identity fallback.
model.set_cond_image(torch.randn(1, 4, 48, 48, generator=torch.Generator().manual_seed(12)))
layer1 = transformer.blocks[0].mlp.layer1
x5 = torch.randn(1, 1, 2, 2, IN_DIM, generator=torch.Generator().manual_seed(13))
assert torch.equal(layer1(x5), plain_linear(layer1, x5))
def test_forward_casts_mismatched_input_dtype() -> None:
model, transformer = make_model_and_transformer()
model.apply_to(transformer)
model.set_multiplier(1.0)
model.set_cond_image(matching_cond_image())
transformer.to(torch.bfloat16)
x = torch.randn(1, 4, IN_DIM, generator=torch.Generator().manual_seed(14)).to(torch.bfloat16)
q_proj = transformer.blocks[0].self_attn.q_proj
y = q_proj(x)
assert y.dtype == torch.bfloat16
assert not torch.equal(y, plain_linear(q_proj, x))
# ----------------------------------------------------------------------------
# Multi-adapter composition (denoise applies in list order, restores LIFO)
# ----------------------------------------------------------------------------
class _IdentityCapture(nn.Linear):
"""nn.Linear whose forward returns its input unchanged — binding a LLLite
module to it reads out the perturbed input the module passes to the
forward it wrapped."""
def forward(self, x: torch.Tensor) -> torch.Tensor:
return x
def make_two_adapters() -> tuple[AnimaControlNetLLLite, AnimaControlNetLLLite, FakeTransformer]:
"""Two adapters with different mlp dims (8 and 16) plus one fake tree."""
model_a = AnimaControlNetLLLite.from_state_dict(make_synthetic_state_dict(seed=0, mlp_dim=8), None)
model_b = AnimaControlNetLLLite.from_state_dict(make_synthetic_state_dict(seed=1, mlp_dim=16), None)
torch.manual_seed(123)
transformer = FakeTransformer(IN_DIM, N_BLOCKS)
return model_a, model_b, transformer
def cond_image_for(seed: int) -> torch.Tensor:
return torch.randn(1, 4, 32, 32, generator=torch.Generator().manual_seed(seed)).clamp(-1, 1)
def test_two_adapters_chain_with_second_wrapping_first() -> None:
model_a, model_b, transformer = make_two_adapters()
assert model_a.mlp_dim != model_b.mlp_dim
q_proj = transformer.blocks[0].self_attn.q_proj
x = torch.randn(1, 4, IN_DIM, generator=torch.Generator().manual_seed(20))
model_a.set_multiplier(1.0)
model_a.set_cond_image(cond_image_for(30))
model_b.set_multiplier(1.0)
model_b.set_cond_image(cond_image_for(31))
# Capture x + delta_B(x): the input B hands to the forward it wrapped.
capture = _IdentityCapture(IN_DIM, IN_DIM, bias=False)
module_b = next(m for m in model_b.lllite_modules if m.lllite_name == "lllite_dit_blocks_0_self_attn_q_proj")
module_b.bind(capture)
try:
x_plus_delta_b = capture(x)
finally:
module_b.unbind()
assert not torch.equal(x_plus_delta_b, x)
model_a.apply_to(transformer)
expected = q_proj(x_plus_delta_b) # A's wrapped forward on B's perturbed input
model_b.apply_to(transformer) # B wraps A
assert torch.equal(q_proj(x), expected)
model_b.restore()
model_a.restore()
def test_two_adapters_reverse_restore_returns_pristine_dispatch() -> None:
model_a, model_b, transformer = make_two_adapters()
x = torch.randn(1, 4, IN_DIM, generator=torch.Generator().manual_seed(21))
targets = [
linear
for block in transformer.blocks
for linear in (block.self_attn.q_proj, block.self_attn.k_proj, block.self_attn.v_proj, block.mlp.layer1)
]
pre_outputs = [linear(x) for linear in targets]
for model, seed in ((model_a, 32), (model_b, 33)):
model.apply_to(transformer)
model.set_multiplier(1.0)
model.set_cond_image(cond_image_for(seed))
assert all("forward" in linear.__dict__ for linear in targets)
# LIFO: the last adapter applied is restored first.
model_b.restore()
model_a.restore()
for linear, expected in zip(targets, pre_outputs, strict=True):
assert "forward" not in linear.__dict__
assert torch.equal(linear(x), expected)
def test_each_adapter_multiplier_gates_only_its_own_contribution() -> None:
model_a, model_b, transformer = make_two_adapters()
q_proj = transformer.blocks[0].self_attn.q_proj
x = torch.randn(1, 4, IN_DIM, generator=torch.Generator().manual_seed(22))
y_plain = plain_linear(q_proj, x)
cond_a = cond_image_for(34)
cond_b = cond_image_for(35)
# Single-adapter baselines.
model_a.apply_to(transformer)
model_a.set_multiplier(1.0)
model_a.set_cond_image(cond_a)
y_a_only = q_proj(x)
model_a.restore()
model_b.apply_to(transformer)
model_b.set_multiplier(1.0)
model_b.set_cond_image(cond_b)
y_b_only = q_proj(x)
model_b.restore()
assert not torch.equal(y_a_only, y_plain)
assert not torch.equal(y_b_only, y_plain)
assert not torch.equal(y_a_only, y_b_only)
# Both applied: zeroing one adapter's multiplier removes exactly its contribution.
model_a.apply_to(transformer)
model_b.apply_to(transformer)
model_a.set_cond_image(cond_a)
model_b.set_cond_image(cond_b)
model_a.set_multiplier(1.0)
model_b.set_multiplier(0.0)
assert torch.equal(q_proj(x), y_a_only)
model_a.set_multiplier(0.0)
model_b.set_multiplier(1.0)
assert torch.equal(q_proj(x), y_b_only)
model_a.set_multiplier(0.0)
model_b.set_multiplier(0.0)
assert torch.equal(q_proj(x), y_plain)
model_a.set_multiplier(1.0)
model_b.set_multiplier(1.0)
y_both = q_proj(x)
assert not torch.equal(y_both, y_a_only)
assert not torch.equal(y_both, y_b_only)
model_b.restore()
model_a.restore()
# ----------------------------------------------------------------------------
# Denoise input normalization (duplicate models share one cached instance)
# ----------------------------------------------------------------------------
def _lllite_field(key: str) -> AnimaLLLiteField:
return AnimaLLLiteField(
image_name="cond_image",
control_model=ModelIdentifierField(
key=key,
hash="blake3:0000",
name=f"adapter-{key}",
base=BaseModelType.Anima,
type=ModelType.ControlNet,
),
)
def test_normalize_control_lllite_to_list() -> None:
assert AnimaDenoiseInvocation._normalize_control_lllite(None) == []
single = _lllite_field("key-a")
assert AnimaDenoiseInvocation._normalize_control_lllite(single) == [single]
pair = [_lllite_field("key-a"), _lllite_field("key-b")]
assert AnimaDenoiseInvocation._normalize_control_lllite(pair) == pair
def test_normalize_control_lllite_sorts_by_model_key() -> None:
"""Apply order must be deterministic: collect-node input order follows random node ids."""
field_a = _lllite_field("key-a")
field_b = _lllite_field("key-b")
assert AnimaDenoiseInvocation._normalize_control_lllite([field_b, field_a]) == [field_a, field_b]
def test_normalize_control_lllite_rejects_duplicate_model_keys() -> None:
pair = [_lllite_field("key-a"), _lllite_field("key-a")]
with pytest.raises(ValueError, match="used by more than one"):
AnimaDenoiseInvocation._normalize_control_lllite(pair)
# ----------------------------------------------------------------------------
# Step-range gate (_get_lllite_multiplier)
# ----------------------------------------------------------------------------
def _stepped_field(weight: float, begin_step_percent: float, end_step_percent: float) -> AnimaLLLiteField:
return AnimaLLLiteField(
image_name="cond_image",
control_model=ModelIdentifierField(
key="key-a",
hash="blake3:0000",
name="adapter",
base=BaseModelType.Anima,
type=ModelType.ControlNet,
),
weight=weight,
begin_step_percent=begin_step_percent,
end_step_percent=end_step_percent,
)
def test_lllite_multiplier_full_range_returns_weight() -> None:
"""A 0%..100% window returns the field weight at every step, boundaries included."""
field = _stepped_field(weight=0.7, begin_step_percent=0.0, end_step_percent=1.0)
for step_index in (0, 1, 10, 19, 20):
assert AnimaDenoiseInvocation._get_lllite_multiplier(field, step_index, total_steps=20) == 0.7
def test_lllite_multiplier_zero_outside_window() -> None:
"""A 25%..75% window over 20 steps gates to first_step=5, last_step=15 (both inclusive)."""
field = _stepped_field(weight=0.5, begin_step_percent=0.25, end_step_percent=0.75)
# Below the window.
assert AnimaDenoiseInvocation._get_lllite_multiplier(field, 4, total_steps=20) == 0.0
# First applied step (floor(0.25 * 20) = 5) is inclusive.
assert AnimaDenoiseInvocation._get_lllite_multiplier(field, 5, total_steps=20) == 0.5
assert AnimaDenoiseInvocation._get_lllite_multiplier(field, 10, total_steps=20) == 0.5
# Last applied step (ceil(0.75 * 20) = 15) is inclusive.
assert AnimaDenoiseInvocation._get_lllite_multiplier(field, 15, total_steps=20) == 0.5
# Above the window.
assert AnimaDenoiseInvocation._get_lllite_multiplier(field, 16, total_steps=20) == 0.0
def test_lllite_multiplier_floor_ceil_rounding() -> None:
"""A non-integer step boundary floors the start and ceils the end (widening, not truncating)."""
# 0.34 * 10 = 3.4 -> first_step = floor(3.4) = 3, last_step = ceil(3.4) = 4.
field = _stepped_field(weight=1.0, begin_step_percent=0.34, end_step_percent=0.34)
assert AnimaDenoiseInvocation._get_lllite_multiplier(field, 2, total_steps=10) == 0.0
assert AnimaDenoiseInvocation._get_lllite_multiplier(field, 3, total_steps=10) == 1.0
assert AnimaDenoiseInvocation._get_lllite_multiplier(field, 4, total_steps=10) == 1.0
assert AnimaDenoiseInvocation._get_lllite_multiplier(field, 5, total_steps=10) == 0.0
# ----------------------------------------------------------------------------
# Real weight file (optional; skipped when the file is not present)
# ----------------------------------------------------------------------------
@pytest.mark.skipif(
REAL_WEIGHTS_PATH is None,
reason=f"set {_REAL_WEIGHTS_ENV_VAR} to the real LLLite weights file to run",
)
def test_from_state_dict_real_file() -> None:
from safetensors import safe_open
from safetensors.torch import load_file
assert REAL_WEIGHTS_PATH is not None
# A stale path must fail loudly, not silently skip.
assert REAL_WEIGHTS_PATH.is_file(), f"{_REAL_WEIGHTS_ENV_VAR} points to a missing file: {REAL_WEIGHTS_PATH}"
sd = load_file(str(REAL_WEIGHTS_PATH))
with safe_open(str(REAL_WEIGHTS_PATH), framework="pt") as f:
metadata = f.metadata()
model = AnimaControlNetLLLite.from_state_dict(sd, metadata)
assert len(model.lllite_modules) == 112
assert model.cond_in_channels == 4
assert model.inpaint_masked_input is True
assert model.cond_emb_dim == 64
assert model.mlp_dim == 64
assert model.cond_dim == 128
assert model.cond_resblocks == 4
assert model.use_aspp is False
for m in model.lllite_modules:
assert m.in_dim == 2048
assert m.down.weight.shape == (64, 2048)
assert m.mid.weight.shape == (64, 128)
assert m.cond_to_film.weight.shape == (128, 64)
assert m.up.weight.shape == (2048, 64)
assert m.depth_embed.shape == (64,)
# Strict loading consumed every saved key (1056 = 48 trunk + 112 * 9).
assert len(model.state_dict()) == len(sd) == 1056
# Forward smoke test on one module bound to a 2048-wide Linear.
model = model.to(torch.float32)
linear = nn.Linear(2048, 2048)
module = model.lllite_modules[0]
module.bind(linear)
try:
model.set_multiplier(1.0)
# Cond image for latent 4x4 -> S = 2*2 = 4 trunk tokens.
model.set_cond_image(torch.randn(1, 4, 32, 32, generator=torch.Generator().manual_seed(15)))
assert module.cond_emb is not None
assert module.cond_emb.shape == (1, 4, 64)
x = torch.randn(1, 4, 2048, generator=torch.Generator().manual_seed(16))
y = linear(x)
assert y.shape == (1, 4, 2048)
assert torch.isfinite(y).all()
assert not torch.equal(y, plain_linear(linear, x))
finally:
module.unbind()
@@ -0,0 +1,195 @@
"""Tests for AnimaSchedulerDriver — the helper that hides per-scheduler API quirks
(sigmas= vs num_inference_steps=, Heun's doubled timestep array, set_begin_index)
behind a uniform iteration interface."""
import inspect
import pytest
import torch
from invokeai.app.invocations.anima_denoise import loglinear_timestep_shift
from invokeai.backend.anima.scheduler_driver import AnimaSchedulerDriver
from invokeai.backend.flux.schedulers import ANIMA_SCHEDULER_MAP, ANIMA_SHIFT
def _anima_sigmas(num_steps: int) -> list[float]:
return [loglinear_timestep_shift(ANIMA_SHIFT, 1.0 - i / num_steps) for i in range(num_steps + 1)]
@pytest.mark.parametrize("scheduler_name", ["heun", "dpmpp_2m", "dpmpp_2m_sde", "er_sde"])
def test_driver_full_schedule_iteration_count(scheduler_name: str) -> None:
"""For full schedules (no clipping), the driver yields enough iterations to
cover one full denoise. Heun yields 2N-1 iterations for N user steps."""
num_steps = 8
sigmas = _anima_sigmas(num_steps)
driver = AnimaSchedulerDriver(
scheduler_name=scheduler_name,
sigmas=sigmas,
steps=num_steps,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=0,
)
iterations = list(driver.iterations())
if driver.is_heun:
assert len(iterations) == 2 * num_steps - 1
else:
assert len(iterations) == num_steps
@pytest.mark.parametrize("scheduler_name", ["dpmpp_2m", "dpmpp_2m_sde", "er_sde"])
def test_driver_single_step_schedulers_complete_user_step_every_iteration(scheduler_name: str) -> None:
"""Non-Heun schedulers report completes_user_step on every iteration."""
num_steps = 6
driver = AnimaSchedulerDriver(
scheduler_name=scheduler_name,
sigmas=_anima_sigmas(num_steps),
steps=num_steps,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=0,
)
user_step_count = sum(1 for it in driver.iterations() if it.completes_user_step)
assert user_step_count == num_steps
def test_driver_heun_completes_user_step_on_second_order_and_terminal() -> None:
"""Heun yields one completion per user step: each pair's 2nd-order half plus
the unpaired terminal 1st-order step (sigma_prev==0)."""
num_steps = 4
driver = AnimaSchedulerDriver(
scheduler_name="heun",
sigmas=_anima_sigmas(num_steps),
steps=num_steps,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=0,
)
# state_in_first_order only toggles once scheduler.step runs, so drive a fake
# step per iteration to mirror production behaviour.
completes_flags = []
for it in driver.iterations():
completes_flags.append(it.completes_user_step)
driver.scheduler.step(
model_output=torch.zeros((1, 1, 1, 4, 4)),
timestep=it.sched_timestep,
sample=torch.zeros((1, 1, 1, 4, 4)),
)
# N=4 → 7 iterations: indices 1, 3, 5 (SO halves) + 6 (terminal FO) = 4 completions.
assert sum(completes_flags) == num_steps
assert completes_flags[-1] is True, "terminal Heun first-order step must complete its user step"
@pytest.mark.parametrize(
("denoising_start", "denoising_end"),
[(0.0, 1.0), (0.2, 1.0), (0.0, 0.8), (0.2, 0.8), (0.5, 0.75)],
)
def test_driver_dpmpp_clipped_schedule_starts_at_correct_sigma(denoising_start: float, denoising_end: float) -> None:
"""DPM++ doesn't accept sigmas= on diffusers 0.35.1; the driver's set_begin_index
fallback must expose a first iteration whose sigma matches the clipped Anima reference.
DPM++ constructs its internal flow schedule via ``np.linspace(1, T, N+1)[:-1]`` rather
than the closed-form Anima loglinear shift, so the leading-edge sigma is offset by up
to ~2e-3 from the Anima reference. That offset is a property of the scheduler family,
not the driver — same offset exists in the pre-refactor code path.
"""
num_steps = 30
full_sigmas = _anima_sigmas(num_steps)
k_start = int(denoising_start * num_steps)
expected_first_sigma = full_sigmas[k_start]
cls, _ = ANIMA_SCHEDULER_MAP["dpmpp_2m"]
accepts_sigmas = "sigmas" in inspect.signature(cls(num_train_timesteps=1000).set_timesteps).parameters
driver = AnimaSchedulerDriver(
scheduler_name="dpmpp_2m",
sigmas=full_sigmas[k_start : int(denoising_end * num_steps) + 1] if accepts_sigmas else full_sigmas,
steps=num_steps,
denoising_start=denoising_start,
denoising_end=denoising_end,
device=torch.device("cpu"),
seed=0,
)
first_iter = next(driver.iterations())
assert abs(first_iter.sigma_curr - expected_first_sigma) < 2e-3
@pytest.mark.parametrize(
("denoising_start", "denoising_end", "steps"),
[(0.2, 0.8, 30), (0.0, 0.8, 30), (0.2, 1.0, 30), (0.5, 0.75, 20)],
)
def test_driver_heun_clipped_schedule_iteration_count(denoising_start: float, denoising_end: float, steps: int) -> None:
"""Heun clipped schedule: iteration count is 2*(k_end-k_start), clamped so
denoising_end=1.0 doesn't run past the 2N-1 array."""
full_sigmas = _anima_sigmas(steps)
k_start = int(denoising_start * steps)
k_end = int(denoising_end * steps)
driver = AnimaSchedulerDriver(
scheduler_name="heun",
sigmas=full_sigmas,
steps=steps,
denoising_start=denoising_start,
denoising_end=denoising_end,
device=torch.device("cpu"),
seed=0,
)
# If Heun's set_timesteps accepts sigmas=, the driver will pass the full schedule directly
# and yield 2*steps-1 iterations regardless of clipping. The set_begin_index path applies
# only when sigmas= is unsupported.
accepts_sigmas = "sigmas" in inspect.signature(driver.scheduler.set_timesteps).parameters
if accepts_sigmas:
# Driver took the sigma-passing path; sigmas were not pre-clipped here, so the count
# reflects the full schedule.
assert driver.num_iterations == 2 * steps - 1
return
expected = min(2 * (k_end - k_start), len(driver.scheduler.timesteps) - driver.begin_index)
assert driver.num_iterations == expected
assert driver.begin_index == 2 * k_start
def test_driver_terminal_sigma_prev_is_zero() -> None:
"""The last iteration's sigma_prev must be 0.0 (terminal noise level)."""
driver = AnimaSchedulerDriver(
scheduler_name="dpmpp_2m",
sigmas=_anima_sigmas(8),
steps=8,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=0,
)
last_iter = list(driver.iterations())[-1]
assert last_iter.sigma_prev == 0.0
def test_driver_seed_determinism() -> None:
"""Same seed → identical step_generator state → reproducible SDE noise."""
sigmas = _anima_sigmas(8)
driver_a = AnimaSchedulerDriver(
scheduler_name="er_sde",
sigmas=sigmas,
steps=8,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=42,
)
driver_b = AnimaSchedulerDriver(
scheduler_name="er_sde",
sigmas=sigmas,
steps=8,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=42,
)
# Same seed → same first random draw.
a = torch.randn((1, 4), generator=driver_a.step_generator)
b = torch.randn((1, 4), generator=driver_b.step_generator)
assert torch.equal(a, b)
+38
View File
@@ -0,0 +1,38 @@
"""Tests for the bundled T5-XXL tokenizer used by Anima.
Anima feeds T5-XXL token IDs into the LLM Adapter's learned embedding table
(nn.Embedding(32128, 1024)). The tokenizer is vendored in the package so users
do not need to install a 9GB T5-XXL encoder just to obtain a ~2MB tokenizer.
"""
from invokeai.backend.anima.t5_tokenizer import ANIMA_T5_VOCAB_SIZE, load_bundled_t5_tokenizer
def test_bundled_tokenizer_is_fast() -> None:
tokenizer = load_bundled_t5_tokenizer()
assert tokenizer.is_fast
def test_bundled_tokenizer_known_ids() -> None:
tokenizer = load_bundled_t5_tokenizer()
ids = tokenizer("a cat sitting on a mat", truncation=True, max_length=512).input_ids
assert ids == [3, 9, 1712, 3823, 30, 3, 9, 6928, 1]
def test_bundled_tokenizer_appends_eos() -> None:
tokenizer = load_bundled_t5_tokenizer()
assert tokenizer("", truncation=True, max_length=512).input_ids == [1]
def test_bundled_tokenizer_ids_within_adapter_embedding() -> None:
tokenizer = load_bundled_t5_tokenizer()
ids = tokenizer(
"a very long and unusual prompt with rare tokens: zxqwv 12345",
truncation=True,
max_length=512,
).input_ids
assert all(0 <= i < ANIMA_T5_VOCAB_SIZE for i in ids)
def test_bundled_tokenizer_is_cached() -> None:
assert load_bundled_t5_tokenizer() is load_bundled_t5_tokenizer()
@@ -0,0 +1,374 @@
# State dict keys and shapes for an InstantX FLUX ControlNet Union model. Intended to be used for unit tests.
# These keys were extracted from:
# https://huggingface.co/InstantX/FLUX.1-dev-Controlnet-Union/blob/4f32d6f2b220f8873d49bb8acc073e1df180c994/diffusion_pytorch_model.safetensors
instantx_sd_shapes = {
"context_embedder.bias": [3072],
"context_embedder.weight": [3072, 4096],
"controlnet_blocks.0.bias": [3072],
"controlnet_blocks.0.weight": [3072, 3072],
"controlnet_blocks.1.bias": [3072],
"controlnet_blocks.1.weight": [3072, 3072],
"controlnet_blocks.2.bias": [3072],
"controlnet_blocks.2.weight": [3072, 3072],
"controlnet_blocks.3.bias": [3072],
"controlnet_blocks.3.weight": [3072, 3072],
"controlnet_blocks.4.bias": [3072],
"controlnet_blocks.4.weight": [3072, 3072],
"controlnet_mode_embedder.weight": [10, 3072],
"controlnet_single_blocks.0.bias": [3072],
"controlnet_single_blocks.0.weight": [3072, 3072],
"controlnet_single_blocks.1.bias": [3072],
"controlnet_single_blocks.1.weight": [3072, 3072],
"controlnet_single_blocks.2.bias": [3072],
"controlnet_single_blocks.2.weight": [3072, 3072],
"controlnet_single_blocks.3.bias": [3072],
"controlnet_single_blocks.3.weight": [3072, 3072],
"controlnet_single_blocks.4.bias": [3072],
"controlnet_single_blocks.4.weight": [3072, 3072],
"controlnet_single_blocks.5.bias": [3072],
"controlnet_single_blocks.5.weight": [3072, 3072],
"controlnet_single_blocks.6.bias": [3072],
"controlnet_single_blocks.6.weight": [3072, 3072],
"controlnet_single_blocks.7.bias": [3072],
"controlnet_single_blocks.7.weight": [3072, 3072],
"controlnet_single_blocks.8.bias": [3072],
"controlnet_single_blocks.8.weight": [3072, 3072],
"controlnet_single_blocks.9.bias": [3072],
"controlnet_single_blocks.9.weight": [3072, 3072],
"controlnet_x_embedder.bias": [3072],
"controlnet_x_embedder.weight": [3072, 64],
"single_transformer_blocks.0.attn.norm_k.weight": [128],
"single_transformer_blocks.0.attn.norm_q.weight": [128],
"single_transformer_blocks.0.attn.to_k.bias": [3072],
"single_transformer_blocks.0.attn.to_k.weight": [3072, 3072],
"single_transformer_blocks.0.attn.to_q.bias": [3072],
"single_transformer_blocks.0.attn.to_q.weight": [3072, 3072],
"single_transformer_blocks.0.attn.to_v.bias": [3072],
"single_transformer_blocks.0.attn.to_v.weight": [3072, 3072],
"single_transformer_blocks.0.norm.linear.bias": [9216],
"single_transformer_blocks.0.norm.linear.weight": [9216, 3072],
"single_transformer_blocks.0.proj_mlp.bias": [12288],
"single_transformer_blocks.0.proj_mlp.weight": [12288, 3072],
"single_transformer_blocks.0.proj_out.bias": [3072],
"single_transformer_blocks.0.proj_out.weight": [3072, 15360],
"single_transformer_blocks.1.attn.norm_k.weight": [128],
"single_transformer_blocks.1.attn.norm_q.weight": [128],
"single_transformer_blocks.1.attn.to_k.bias": [3072],
"single_transformer_blocks.1.attn.to_k.weight": [3072, 3072],
"single_transformer_blocks.1.attn.to_q.bias": [3072],
"single_transformer_blocks.1.attn.to_q.weight": [3072, 3072],
"single_transformer_blocks.1.attn.to_v.bias": [3072],
"single_transformer_blocks.1.attn.to_v.weight": [3072, 3072],
"single_transformer_blocks.1.norm.linear.bias": [9216],
"single_transformer_blocks.1.norm.linear.weight": [9216, 3072],
"single_transformer_blocks.1.proj_mlp.bias": [12288],
"single_transformer_blocks.1.proj_mlp.weight": [12288, 3072],
"single_transformer_blocks.1.proj_out.bias": [3072],
"single_transformer_blocks.1.proj_out.weight": [3072, 15360],
"single_transformer_blocks.2.attn.norm_k.weight": [128],
"single_transformer_blocks.2.attn.norm_q.weight": [128],
"single_transformer_blocks.2.attn.to_k.bias": [3072],
"single_transformer_blocks.2.attn.to_k.weight": [3072, 3072],
"single_transformer_blocks.2.attn.to_q.bias": [3072],
"single_transformer_blocks.2.attn.to_q.weight": [3072, 3072],
"single_transformer_blocks.2.attn.to_v.bias": [3072],
"single_transformer_blocks.2.attn.to_v.weight": [3072, 3072],
"single_transformer_blocks.2.norm.linear.bias": [9216],
"single_transformer_blocks.2.norm.linear.weight": [9216, 3072],
"single_transformer_blocks.2.proj_mlp.bias": [12288],
"single_transformer_blocks.2.proj_mlp.weight": [12288, 3072],
"single_transformer_blocks.2.proj_out.bias": [3072],
"single_transformer_blocks.2.proj_out.weight": [3072, 15360],
"single_transformer_blocks.3.attn.norm_k.weight": [128],
"single_transformer_blocks.3.attn.norm_q.weight": [128],
"single_transformer_blocks.3.attn.to_k.bias": [3072],
"single_transformer_blocks.3.attn.to_k.weight": [3072, 3072],
"single_transformer_blocks.3.attn.to_q.bias": [3072],
"single_transformer_blocks.3.attn.to_q.weight": [3072, 3072],
"single_transformer_blocks.3.attn.to_v.bias": [3072],
"single_transformer_blocks.3.attn.to_v.weight": [3072, 3072],
"single_transformer_blocks.3.norm.linear.bias": [9216],
"single_transformer_blocks.3.norm.linear.weight": [9216, 3072],
"single_transformer_blocks.3.proj_mlp.bias": [12288],
"single_transformer_blocks.3.proj_mlp.weight": [12288, 3072],
"single_transformer_blocks.3.proj_out.bias": [3072],
"single_transformer_blocks.3.proj_out.weight": [3072, 15360],
"single_transformer_blocks.4.attn.norm_k.weight": [128],
"single_transformer_blocks.4.attn.norm_q.weight": [128],
"single_transformer_blocks.4.attn.to_k.bias": [3072],
"single_transformer_blocks.4.attn.to_k.weight": [3072, 3072],
"single_transformer_blocks.4.attn.to_q.bias": [3072],
"single_transformer_blocks.4.attn.to_q.weight": [3072, 3072],
"single_transformer_blocks.4.attn.to_v.bias": [3072],
"single_transformer_blocks.4.attn.to_v.weight": [3072, 3072],
"single_transformer_blocks.4.norm.linear.bias": [9216],
"single_transformer_blocks.4.norm.linear.weight": [9216, 3072],
"single_transformer_blocks.4.proj_mlp.bias": [12288],
"single_transformer_blocks.4.proj_mlp.weight": [12288, 3072],
"single_transformer_blocks.4.proj_out.bias": [3072],
"single_transformer_blocks.4.proj_out.weight": [3072, 15360],
"single_transformer_blocks.5.attn.norm_k.weight": [128],
"single_transformer_blocks.5.attn.norm_q.weight": [128],
"single_transformer_blocks.5.attn.to_k.bias": [3072],
"single_transformer_blocks.5.attn.to_k.weight": [3072, 3072],
"single_transformer_blocks.5.attn.to_q.bias": [3072],
"single_transformer_blocks.5.attn.to_q.weight": [3072, 3072],
"single_transformer_blocks.5.attn.to_v.bias": [3072],
"single_transformer_blocks.5.attn.to_v.weight": [3072, 3072],
"single_transformer_blocks.5.norm.linear.bias": [9216],
"single_transformer_blocks.5.norm.linear.weight": [9216, 3072],
"single_transformer_blocks.5.proj_mlp.bias": [12288],
"single_transformer_blocks.5.proj_mlp.weight": [12288, 3072],
"single_transformer_blocks.5.proj_out.bias": [3072],
"single_transformer_blocks.5.proj_out.weight": [3072, 15360],
"single_transformer_blocks.6.attn.norm_k.weight": [128],
"single_transformer_blocks.6.attn.norm_q.weight": [128],
"single_transformer_blocks.6.attn.to_k.bias": [3072],
"single_transformer_blocks.6.attn.to_k.weight": [3072, 3072],
"single_transformer_blocks.6.attn.to_q.bias": [3072],
"single_transformer_blocks.6.attn.to_q.weight": [3072, 3072],
"single_transformer_blocks.6.attn.to_v.bias": [3072],
"single_transformer_blocks.6.attn.to_v.weight": [3072, 3072],
"single_transformer_blocks.6.norm.linear.bias": [9216],
"single_transformer_blocks.6.norm.linear.weight": [9216, 3072],
"single_transformer_blocks.6.proj_mlp.bias": [12288],
"single_transformer_blocks.6.proj_mlp.weight": [12288, 3072],
"single_transformer_blocks.6.proj_out.bias": [3072],
"single_transformer_blocks.6.proj_out.weight": [3072, 15360],
"single_transformer_blocks.7.attn.norm_k.weight": [128],
"single_transformer_blocks.7.attn.norm_q.weight": [128],
"single_transformer_blocks.7.attn.to_k.bias": [3072],
"single_transformer_blocks.7.attn.to_k.weight": [3072, 3072],
"single_transformer_blocks.7.attn.to_q.bias": [3072],
"single_transformer_blocks.7.attn.to_q.weight": [3072, 3072],
"single_transformer_blocks.7.attn.to_v.bias": [3072],
"single_transformer_blocks.7.attn.to_v.weight": [3072, 3072],
"single_transformer_blocks.7.norm.linear.bias": [9216],
"single_transformer_blocks.7.norm.linear.weight": [9216, 3072],
"single_transformer_blocks.7.proj_mlp.bias": [12288],
"single_transformer_blocks.7.proj_mlp.weight": [12288, 3072],
"single_transformer_blocks.7.proj_out.bias": [3072],
"single_transformer_blocks.7.proj_out.weight": [3072, 15360],
"single_transformer_blocks.8.attn.norm_k.weight": [128],
"single_transformer_blocks.8.attn.norm_q.weight": [128],
"single_transformer_blocks.8.attn.to_k.bias": [3072],
"single_transformer_blocks.8.attn.to_k.weight": [3072, 3072],
"single_transformer_blocks.8.attn.to_q.bias": [3072],
"single_transformer_blocks.8.attn.to_q.weight": [3072, 3072],
"single_transformer_blocks.8.attn.to_v.bias": [3072],
"single_transformer_blocks.8.attn.to_v.weight": [3072, 3072],
"single_transformer_blocks.8.norm.linear.bias": [9216],
"single_transformer_blocks.8.norm.linear.weight": [9216, 3072],
"single_transformer_blocks.8.proj_mlp.bias": [12288],
"single_transformer_blocks.8.proj_mlp.weight": [12288, 3072],
"single_transformer_blocks.8.proj_out.bias": [3072],
"single_transformer_blocks.8.proj_out.weight": [3072, 15360],
"single_transformer_blocks.9.attn.norm_k.weight": [128],
"single_transformer_blocks.9.attn.norm_q.weight": [128],
"single_transformer_blocks.9.attn.to_k.bias": [3072],
"single_transformer_blocks.9.attn.to_k.weight": [3072, 3072],
"single_transformer_blocks.9.attn.to_q.bias": [3072],
"single_transformer_blocks.9.attn.to_q.weight": [3072, 3072],
"single_transformer_blocks.9.attn.to_v.bias": [3072],
"single_transformer_blocks.9.attn.to_v.weight": [3072, 3072],
"single_transformer_blocks.9.norm.linear.bias": [9216],
"single_transformer_blocks.9.norm.linear.weight": [9216, 3072],
"single_transformer_blocks.9.proj_mlp.bias": [12288],
"single_transformer_blocks.9.proj_mlp.weight": [12288, 3072],
"single_transformer_blocks.9.proj_out.bias": [3072],
"single_transformer_blocks.9.proj_out.weight": [3072, 15360],
"time_text_embed.guidance_embedder.linear_1.bias": [3072],
"time_text_embed.guidance_embedder.linear_1.weight": [3072, 256],
"time_text_embed.guidance_embedder.linear_2.bias": [3072],
"time_text_embed.guidance_embedder.linear_2.weight": [3072, 3072],
"time_text_embed.text_embedder.linear_1.bias": [3072],
"time_text_embed.text_embedder.linear_1.weight": [3072, 768],
"time_text_embed.text_embedder.linear_2.bias": [3072],
"time_text_embed.text_embedder.linear_2.weight": [3072, 3072],
"time_text_embed.timestep_embedder.linear_1.bias": [3072],
"time_text_embed.timestep_embedder.linear_1.weight": [3072, 256],
"time_text_embed.timestep_embedder.linear_2.bias": [3072],
"time_text_embed.timestep_embedder.linear_2.weight": [3072, 3072],
"transformer_blocks.0.attn.add_k_proj.bias": [3072],
"transformer_blocks.0.attn.add_k_proj.weight": [3072, 3072],
"transformer_blocks.0.attn.add_q_proj.bias": [3072],
"transformer_blocks.0.attn.add_q_proj.weight": [3072, 3072],
"transformer_blocks.0.attn.add_v_proj.bias": [3072],
"transformer_blocks.0.attn.add_v_proj.weight": [3072, 3072],
"transformer_blocks.0.attn.norm_added_k.weight": [128],
"transformer_blocks.0.attn.norm_added_q.weight": [128],
"transformer_blocks.0.attn.norm_k.weight": [128],
"transformer_blocks.0.attn.norm_q.weight": [128],
"transformer_blocks.0.attn.to_add_out.bias": [3072],
"transformer_blocks.0.attn.to_add_out.weight": [3072, 3072],
"transformer_blocks.0.attn.to_k.bias": [3072],
"transformer_blocks.0.attn.to_k.weight": [3072, 3072],
"transformer_blocks.0.attn.to_out.0.bias": [3072],
"transformer_blocks.0.attn.to_out.0.weight": [3072, 3072],
"transformer_blocks.0.attn.to_q.bias": [3072],
"transformer_blocks.0.attn.to_q.weight": [3072, 3072],
"transformer_blocks.0.attn.to_v.bias": [3072],
"transformer_blocks.0.attn.to_v.weight": [3072, 3072],
"transformer_blocks.0.ff.net.0.proj.bias": [12288],
"transformer_blocks.0.ff.net.0.proj.weight": [12288, 3072],
"transformer_blocks.0.ff.net.2.bias": [3072],
"transformer_blocks.0.ff.net.2.weight": [3072, 12288],
"transformer_blocks.0.ff_context.net.0.proj.bias": [12288],
"transformer_blocks.0.ff_context.net.0.proj.weight": [12288, 3072],
"transformer_blocks.0.ff_context.net.2.bias": [3072],
"transformer_blocks.0.ff_context.net.2.weight": [3072, 12288],
"transformer_blocks.0.norm1.linear.bias": [18432],
"transformer_blocks.0.norm1.linear.weight": [18432, 3072],
"transformer_blocks.0.norm1_context.linear.bias": [18432],
"transformer_blocks.0.norm1_context.linear.weight": [18432, 3072],
"transformer_blocks.1.attn.add_k_proj.bias": [3072],
"transformer_blocks.1.attn.add_k_proj.weight": [3072, 3072],
"transformer_blocks.1.attn.add_q_proj.bias": [3072],
"transformer_blocks.1.attn.add_q_proj.weight": [3072, 3072],
"transformer_blocks.1.attn.add_v_proj.bias": [3072],
"transformer_blocks.1.attn.add_v_proj.weight": [3072, 3072],
"transformer_blocks.1.attn.norm_added_k.weight": [128],
"transformer_blocks.1.attn.norm_added_q.weight": [128],
"transformer_blocks.1.attn.norm_k.weight": [128],
"transformer_blocks.1.attn.norm_q.weight": [128],
"transformer_blocks.1.attn.to_add_out.bias": [3072],
"transformer_blocks.1.attn.to_add_out.weight": [3072, 3072],
"transformer_blocks.1.attn.to_k.bias": [3072],
"transformer_blocks.1.attn.to_k.weight": [3072, 3072],
"transformer_blocks.1.attn.to_out.0.bias": [3072],
"transformer_blocks.1.attn.to_out.0.weight": [3072, 3072],
"transformer_blocks.1.attn.to_q.bias": [3072],
"transformer_blocks.1.attn.to_q.weight": [3072, 3072],
"transformer_blocks.1.attn.to_v.bias": [3072],
"transformer_blocks.1.attn.to_v.weight": [3072, 3072],
"transformer_blocks.1.ff.net.0.proj.bias": [12288],
"transformer_blocks.1.ff.net.0.proj.weight": [12288, 3072],
"transformer_blocks.1.ff.net.2.bias": [3072],
"transformer_blocks.1.ff.net.2.weight": [3072, 12288],
"transformer_blocks.1.ff_context.net.0.proj.bias": [12288],
"transformer_blocks.1.ff_context.net.0.proj.weight": [12288, 3072],
"transformer_blocks.1.ff_context.net.2.bias": [3072],
"transformer_blocks.1.ff_context.net.2.weight": [3072, 12288],
"transformer_blocks.1.norm1.linear.bias": [18432],
"transformer_blocks.1.norm1.linear.weight": [18432, 3072],
"transformer_blocks.1.norm1_context.linear.bias": [18432],
"transformer_blocks.1.norm1_context.linear.weight": [18432, 3072],
"transformer_blocks.2.attn.add_k_proj.bias": [3072],
"transformer_blocks.2.attn.add_k_proj.weight": [3072, 3072],
"transformer_blocks.2.attn.add_q_proj.bias": [3072],
"transformer_blocks.2.attn.add_q_proj.weight": [3072, 3072],
"transformer_blocks.2.attn.add_v_proj.bias": [3072],
"transformer_blocks.2.attn.add_v_proj.weight": [3072, 3072],
"transformer_blocks.2.attn.norm_added_k.weight": [128],
"transformer_blocks.2.attn.norm_added_q.weight": [128],
"transformer_blocks.2.attn.norm_k.weight": [128],
"transformer_blocks.2.attn.norm_q.weight": [128],
"transformer_blocks.2.attn.to_add_out.bias": [3072],
"transformer_blocks.2.attn.to_add_out.weight": [3072, 3072],
"transformer_blocks.2.attn.to_k.bias": [3072],
"transformer_blocks.2.attn.to_k.weight": [3072, 3072],
"transformer_blocks.2.attn.to_out.0.bias": [3072],
"transformer_blocks.2.attn.to_out.0.weight": [3072, 3072],
"transformer_blocks.2.attn.to_q.bias": [3072],
"transformer_blocks.2.attn.to_q.weight": [3072, 3072],
"transformer_blocks.2.attn.to_v.bias": [3072],
"transformer_blocks.2.attn.to_v.weight": [3072, 3072],
"transformer_blocks.2.ff.net.0.proj.bias": [12288],
"transformer_blocks.2.ff.net.0.proj.weight": [12288, 3072],
"transformer_blocks.2.ff.net.2.bias": [3072],
"transformer_blocks.2.ff.net.2.weight": [3072, 12288],
"transformer_blocks.2.ff_context.net.0.proj.bias": [12288],
"transformer_blocks.2.ff_context.net.0.proj.weight": [12288, 3072],
"transformer_blocks.2.ff_context.net.2.bias": [3072],
"transformer_blocks.2.ff_context.net.2.weight": [3072, 12288],
"transformer_blocks.2.norm1.linear.bias": [18432],
"transformer_blocks.2.norm1.linear.weight": [18432, 3072],
"transformer_blocks.2.norm1_context.linear.bias": [18432],
"transformer_blocks.2.norm1_context.linear.weight": [18432, 3072],
"transformer_blocks.3.attn.add_k_proj.bias": [3072],
"transformer_blocks.3.attn.add_k_proj.weight": [3072, 3072],
"transformer_blocks.3.attn.add_q_proj.bias": [3072],
"transformer_blocks.3.attn.add_q_proj.weight": [3072, 3072],
"transformer_blocks.3.attn.add_v_proj.bias": [3072],
"transformer_blocks.3.attn.add_v_proj.weight": [3072, 3072],
"transformer_blocks.3.attn.norm_added_k.weight": [128],
"transformer_blocks.3.attn.norm_added_q.weight": [128],
"transformer_blocks.3.attn.norm_k.weight": [128],
"transformer_blocks.3.attn.norm_q.weight": [128],
"transformer_blocks.3.attn.to_add_out.bias": [3072],
"transformer_blocks.3.attn.to_add_out.weight": [3072, 3072],
"transformer_blocks.3.attn.to_k.bias": [3072],
"transformer_blocks.3.attn.to_k.weight": [3072, 3072],
"transformer_blocks.3.attn.to_out.0.bias": [3072],
"transformer_blocks.3.attn.to_out.0.weight": [3072, 3072],
"transformer_blocks.3.attn.to_q.bias": [3072],
"transformer_blocks.3.attn.to_q.weight": [3072, 3072],
"transformer_blocks.3.attn.to_v.bias": [3072],
"transformer_blocks.3.attn.to_v.weight": [3072, 3072],
"transformer_blocks.3.ff.net.0.proj.bias": [12288],
"transformer_blocks.3.ff.net.0.proj.weight": [12288, 3072],
"transformer_blocks.3.ff.net.2.bias": [3072],
"transformer_blocks.3.ff.net.2.weight": [3072, 12288],
"transformer_blocks.3.ff_context.net.0.proj.bias": [12288],
"transformer_blocks.3.ff_context.net.0.proj.weight": [12288, 3072],
"transformer_blocks.3.ff_context.net.2.bias": [3072],
"transformer_blocks.3.ff_context.net.2.weight": [3072, 12288],
"transformer_blocks.3.norm1.linear.bias": [18432],
"transformer_blocks.3.norm1.linear.weight": [18432, 3072],
"transformer_blocks.3.norm1_context.linear.bias": [18432],
"transformer_blocks.3.norm1_context.linear.weight": [18432, 3072],
"transformer_blocks.4.attn.add_k_proj.bias": [3072],
"transformer_blocks.4.attn.add_k_proj.weight": [3072, 3072],
"transformer_blocks.4.attn.add_q_proj.bias": [3072],
"transformer_blocks.4.attn.add_q_proj.weight": [3072, 3072],
"transformer_blocks.4.attn.add_v_proj.bias": [3072],
"transformer_blocks.4.attn.add_v_proj.weight": [3072, 3072],
"transformer_blocks.4.attn.norm_added_k.weight": [128],
"transformer_blocks.4.attn.norm_added_q.weight": [128],
"transformer_blocks.4.attn.norm_k.weight": [128],
"transformer_blocks.4.attn.norm_q.weight": [128],
"transformer_blocks.4.attn.to_add_out.bias": [3072],
"transformer_blocks.4.attn.to_add_out.weight": [3072, 3072],
"transformer_blocks.4.attn.to_k.bias": [3072],
"transformer_blocks.4.attn.to_k.weight": [3072, 3072],
"transformer_blocks.4.attn.to_out.0.bias": [3072],
"transformer_blocks.4.attn.to_out.0.weight": [3072, 3072],
"transformer_blocks.4.attn.to_q.bias": [3072],
"transformer_blocks.4.attn.to_q.weight": [3072, 3072],
"transformer_blocks.4.attn.to_v.bias": [3072],
"transformer_blocks.4.attn.to_v.weight": [3072, 3072],
"transformer_blocks.4.ff.net.0.proj.bias": [12288],
"transformer_blocks.4.ff.net.0.proj.weight": [12288, 3072],
"transformer_blocks.4.ff.net.2.bias": [3072],
"transformer_blocks.4.ff.net.2.weight": [3072, 12288],
"transformer_blocks.4.ff_context.net.0.proj.bias": [12288],
"transformer_blocks.4.ff_context.net.0.proj.weight": [12288, 3072],
"transformer_blocks.4.ff_context.net.2.bias": [3072],
"transformer_blocks.4.ff_context.net.2.weight": [3072, 12288],
"transformer_blocks.4.norm1.linear.bias": [18432],
"transformer_blocks.4.norm1.linear.weight": [18432, 3072],
"transformer_blocks.4.norm1_context.linear.bias": [18432],
"transformer_blocks.4.norm1_context.linear.weight": [18432, 3072],
"x_embedder.bias": [3072],
"x_embedder.weight": [3072, 64],
}
# InstantX FLUX ControlNet config for unit tests.
# Copied from https://huggingface.co/InstantX/FLUX.1-dev-Controlnet-Union/blob/main/config.json
instantx_config = {
"_class_name": "FluxControlNetModel",
"_diffusers_version": "0.30.0.dev0",
"_name_or_path": "/mnt/wangqixun/",
"attention_head_dim": 128,
"axes_dims_rope": [16, 56, 56],
"guidance_embeds": True,
"in_channels": 64,
"joint_attention_dim": 4096,
"num_attention_heads": 24,
"num_layers": 5,
"num_mode": 10,
"num_single_layers": 10,
"patch_size": 1,
"pooled_projection_dim": 768,
}
@@ -0,0 +1,108 @@
import sys
import pytest
import torch
from invokeai.backend.flux.controlnet.instantx_controlnet_flux import InstantXControlNetFlux
from invokeai.backend.flux.controlnet.state_dict_utils import (
convert_diffusers_instantx_state_dict_to_bfl_format,
infer_flux_params_from_state_dict,
infer_instantx_num_control_modes_from_state_dict,
is_state_dict_instantx_controlnet,
is_state_dict_xlabs_controlnet,
)
from tests.backend.flux.controlnet.instantx_flux_controlnet_state_dict import instantx_config, instantx_sd_shapes
from tests.backend.flux.controlnet.xlabs_flux_controlnet_state_dict import xlabs_sd_shapes
@pytest.mark.parametrize(
["sd_shapes", "expected"],
[
(xlabs_sd_shapes, True),
(instantx_sd_shapes, False),
(["foo"], False),
],
)
def test_is_state_dict_xlabs_controlnet(sd_shapes: dict[str, list[int]], expected: bool):
sd = dict.fromkeys(sd_shapes)
assert is_state_dict_xlabs_controlnet(sd) == expected
@pytest.mark.parametrize(
["sd_keys", "expected"],
[
(instantx_sd_shapes, True),
(xlabs_sd_shapes, False),
(["foo"], False),
],
)
def test_is_state_dict_instantx_controlnet(sd_keys: list[str], expected: bool):
sd = dict.fromkeys(sd_keys)
assert is_state_dict_instantx_controlnet(sd) == expected
def test_convert_diffusers_instantx_state_dict_to_bfl_format():
"""Smoke test convert_diffusers_instantx_state_dict_to_bfl_format() to ensure that it handles all of the keys."""
sd = {k: torch.zeros(1) for k in instantx_sd_shapes}
bfl_sd = convert_diffusers_instantx_state_dict_to_bfl_format(sd)
assert bfl_sd is not None
# TODO(ryand): Figure out why some tests in this file are failing on the MacOS CI runners. It seems to be related to
# using the meta device. I can't reproduce the issue on my local MacOS system.
@pytest.mark.skipif(sys.platform == "darwin", reason="Skipping on macOS")
def test_infer_flux_params_from_state_dict():
# Construct a dummy state_dict with tensors of the correct shape on the meta device.
with torch.device("meta"):
sd = {k: torch.zeros(v) for k, v in instantx_sd_shapes.items()}
sd = convert_diffusers_instantx_state_dict_to_bfl_format(sd)
flux_params = infer_flux_params_from_state_dict(sd)
assert flux_params.in_channels == instantx_config["in_channels"]
assert flux_params.vec_in_dim == instantx_config["pooled_projection_dim"]
assert flux_params.context_in_dim == instantx_config["joint_attention_dim"]
assert flux_params.hidden_size // flux_params.num_heads == instantx_config["attention_head_dim"]
assert flux_params.num_heads == instantx_config["num_attention_heads"]
assert flux_params.mlp_ratio == 4
assert flux_params.depth == instantx_config["num_layers"]
assert flux_params.depth_single_blocks == instantx_config["num_single_layers"]
assert flux_params.axes_dim == instantx_config["axes_dims_rope"]
assert flux_params.theta == 10000
assert flux_params.qkv_bias
assert flux_params.guidance_embed == instantx_config["guidance_embeds"]
@pytest.mark.skipif(sys.platform == "darwin", reason="Skipping on macOS")
def test_infer_instantx_num_control_modes_from_state_dict():
# Construct a dummy state_dict with tensors of the correct shape on the meta device.
with torch.device("meta"):
sd = {k: torch.zeros(v) for k, v in instantx_sd_shapes.items()}
sd = convert_diffusers_instantx_state_dict_to_bfl_format(sd)
num_control_modes = infer_instantx_num_control_modes_from_state_dict(sd)
assert num_control_modes == instantx_config["num_mode"]
@pytest.mark.skipif(sys.platform == "darwin", reason="Skipping on macOS")
def test_load_instantx_from_state_dict():
# Construct a dummy state_dict with tensors of the correct shape on the meta device.
with torch.device("meta"):
sd = {k: torch.zeros(v) for k, v in instantx_sd_shapes.items()}
sd = convert_diffusers_instantx_state_dict_to_bfl_format(sd)
flux_params = infer_flux_params_from_state_dict(sd)
num_control_modes = infer_instantx_num_control_modes_from_state_dict(sd)
with torch.device("meta"):
model = InstantXControlNetFlux(flux_params, num_control_modes)
model_sd = model.state_dict()
assert set(model_sd.keys()) == set(sd.keys())
for key, tensor in model_sd.items():
assert isinstance(tensor, torch.Tensor)
assert tensor.shape == sd[key].shape
@@ -0,0 +1,91 @@
# State dict keys and shapes for an XLabs FLUX ControlNet model. Intended to be used for unit tests.
# These keys were extracted from:
# https://huggingface.co/XLabs-AI/flux-controlnet-collections/blob/86ab1e915a389d5857135c00e0d350e9e38a9048/flux-canny-controlnet_v2.safetensors
xlabs_sd_shapes = {
"controlnet_blocks.0.bias": [3072],
"controlnet_blocks.0.weight": [3072, 3072],
"controlnet_blocks.1.bias": [3072],
"controlnet_blocks.1.weight": [3072, 3072],
"double_blocks.0.img_attn.norm.key_norm.scale": [128],
"double_blocks.0.img_attn.norm.query_norm.scale": [128],
"double_blocks.0.img_attn.proj.bias": [3072],
"double_blocks.0.img_attn.proj.weight": [3072, 3072],
"double_blocks.0.img_attn.qkv.bias": [9216],
"double_blocks.0.img_attn.qkv.weight": [9216, 3072],
"double_blocks.0.img_mlp.0.bias": [12288],
"double_blocks.0.img_mlp.0.weight": [12288, 3072],
"double_blocks.0.img_mlp.2.bias": [3072],
"double_blocks.0.img_mlp.2.weight": [3072, 12288],
"double_blocks.0.img_mod.lin.bias": [18432],
"double_blocks.0.img_mod.lin.weight": [18432, 3072],
"double_blocks.0.txt_attn.norm.key_norm.scale": [128],
"double_blocks.0.txt_attn.norm.query_norm.scale": [128],
"double_blocks.0.txt_attn.proj.bias": [3072],
"double_blocks.0.txt_attn.proj.weight": [3072, 3072],
"double_blocks.0.txt_attn.qkv.bias": [9216],
"double_blocks.0.txt_attn.qkv.weight": [9216, 3072],
"double_blocks.0.txt_mlp.0.bias": [12288],
"double_blocks.0.txt_mlp.0.weight": [12288, 3072],
"double_blocks.0.txt_mlp.2.bias": [3072],
"double_blocks.0.txt_mlp.2.weight": [3072, 12288],
"double_blocks.0.txt_mod.lin.bias": [18432],
"double_blocks.0.txt_mod.lin.weight": [18432, 3072],
"double_blocks.1.img_attn.norm.key_norm.scale": [128],
"double_blocks.1.img_attn.norm.query_norm.scale": [128],
"double_blocks.1.img_attn.proj.bias": [3072],
"double_blocks.1.img_attn.proj.weight": [3072, 3072],
"double_blocks.1.img_attn.qkv.bias": [9216],
"double_blocks.1.img_attn.qkv.weight": [9216, 3072],
"double_blocks.1.img_mlp.0.bias": [12288],
"double_blocks.1.img_mlp.0.weight": [12288, 3072],
"double_blocks.1.img_mlp.2.bias": [3072],
"double_blocks.1.img_mlp.2.weight": [3072, 12288],
"double_blocks.1.img_mod.lin.bias": [18432],
"double_blocks.1.img_mod.lin.weight": [18432, 3072],
"double_blocks.1.txt_attn.norm.key_norm.scale": [128],
"double_blocks.1.txt_attn.norm.query_norm.scale": [128],
"double_blocks.1.txt_attn.proj.bias": [3072],
"double_blocks.1.txt_attn.proj.weight": [3072, 3072],
"double_blocks.1.txt_attn.qkv.bias": [9216],
"double_blocks.1.txt_attn.qkv.weight": [9216, 3072],
"double_blocks.1.txt_mlp.0.bias": [12288],
"double_blocks.1.txt_mlp.0.weight": [12288, 3072],
"double_blocks.1.txt_mlp.2.bias": [3072],
"double_blocks.1.txt_mlp.2.weight": [3072, 12288],
"double_blocks.1.txt_mod.lin.bias": [18432],
"double_blocks.1.txt_mod.lin.weight": [18432, 3072],
"guidance_in.in_layer.bias": [3072],
"guidance_in.in_layer.weight": [3072, 256],
"guidance_in.out_layer.bias": [3072],
"guidance_in.out_layer.weight": [3072, 3072],
"img_in.bias": [3072],
"img_in.weight": [3072, 64],
"input_hint_block.0.bias": [16],
"input_hint_block.0.weight": [16, 3, 3, 3],
"input_hint_block.10.bias": [16],
"input_hint_block.10.weight": [16, 16, 3, 3],
"input_hint_block.12.bias": [16],
"input_hint_block.12.weight": [16, 16, 3, 3],
"input_hint_block.14.bias": [16],
"input_hint_block.14.weight": [16, 16, 3, 3],
"input_hint_block.2.bias": [16],
"input_hint_block.2.weight": [16, 16, 3, 3],
"input_hint_block.4.bias": [16],
"input_hint_block.4.weight": [16, 16, 3, 3],
"input_hint_block.6.bias": [16],
"input_hint_block.6.weight": [16, 16, 3, 3],
"input_hint_block.8.bias": [16],
"input_hint_block.8.weight": [16, 16, 3, 3],
"pos_embed_input.bias": [3072],
"pos_embed_input.weight": [3072, 64],
"time_in.in_layer.bias": [3072],
"time_in.in_layer.weight": [3072, 256],
"time_in.out_layer.bias": [3072],
"time_in.out_layer.weight": [3072, 3072],
"txt_in.bias": [3072],
"txt_in.weight": [3072, 4096],
"vector_in.in_layer.bias": [3072],
"vector_in.in_layer.weight": [3072, 768],
"vector_in.out_layer.bias": [3072],
"vector_in.out_layer.weight": [3072, 3072],
}
+499
View File
@@ -0,0 +1,499 @@
"""Tests for DyPE (Dynamic Position Extrapolation) module."""
import torch
from invokeai.backend.flux.dype.base import (
DyPEConfig,
compute_vision_yarn_freqs,
get_timestep_kappa,
)
from invokeai.backend.flux.dype.embed import DyPEEmbedND
from invokeai.backend.flux.dype.presets import (
DYPE_PRESET_4K,
DYPE_PRESET_AREA,
DYPE_PRESET_AUTO,
DYPE_PRESET_MANUAL,
DYPE_PRESET_OFF,
DYPE_PRESETS,
get_dype_config_for_area,
get_dype_config_for_resolution,
get_dype_config_from_preset,
)
from invokeai.backend.flux.dype.rope import rope_dype
from invokeai.backend.flux.extensions.dype_extension import DyPEExtension
class TestDyPEConfig:
"""Tests for DyPEConfig dataclass."""
def test_default_values(self):
config = DyPEConfig()
assert config.enable_dype is True
assert config.base_resolution == 1024
assert config.dype_scale == 2.0
assert config.dype_exponent == 2.0
assert config.dype_start_sigma == 1.0
def test_custom_values(self):
config = DyPEConfig(
enable_dype=False,
base_resolution=512,
dype_scale=4.0,
dype_exponent=3.0,
dype_start_sigma=0.5,
)
assert config.enable_dype is False
assert config.base_resolution == 512
assert config.dype_scale == 4.0
class TestDyPEExtension:
"""Tests for DyPE extension helpers."""
def test_resolve_step_sigma_prefers_scheduler_sigmas_tensor(self):
sigma = DyPEExtension.resolve_step_sigma(
fallback_sigma=0.42,
step_index=1,
scheduler_sigmas=torch.tensor([1.0, 0.75, 0.5]),
)
assert sigma == 0.75
def test_resolve_step_sigma_falls_back_without_scheduler_sigmas(self):
sigma = DyPEExtension.resolve_step_sigma(
fallback_sigma=0.42,
step_index=1,
scheduler_sigmas=None,
)
assert sigma == 0.42
class TestKappa:
"""Tests for the DyPE timestep scheduler."""
def test_get_timestep_kappa_clamps_to_zero_without_scale(self):
assert (
get_timestep_kappa(
current_sigma=0.5,
dype_scale=0.0,
dype_exponent=2.0,
dype_start_sigma=1.0,
)
== 0.0
)
def test_get_timestep_kappa_is_stronger_early(self):
early_kappa = get_timestep_kappa(
current_sigma=1.0,
dype_scale=2.0,
dype_exponent=2.0,
dype_start_sigma=1.0,
)
late_kappa = get_timestep_kappa(
current_sigma=0.1,
dype_scale=2.0,
dype_exponent=2.0,
dype_start_sigma=1.0,
)
assert early_kappa == 2.0
assert late_kappa < early_kappa
def test_get_timestep_kappa_clamps_above_start_sigma(self):
kappa = get_timestep_kappa(
current_sigma=2.0,
dype_scale=2.0,
dype_exponent=2.0,
dype_start_sigma=1.0,
)
assert kappa == 2.0
class TestRopeDype:
"""Tests for DyPE-enhanced RoPE function."""
def test_rope_dype_shape(self):
"""Test that rope_dype returns correct shape."""
pos = torch.zeros(1, 64)
dim = 64
theta = 10000
config = DyPEConfig()
result = rope_dype(
pos=pos,
dim=dim,
theta=theta,
current_sigma=0.5,
target_height=2048,
target_width=2048,
dype_config=config,
)
# Shape should be (batch, seq_len, dim/2, 2, 2)
assert result.shape == (1, 64, dim // 2, 2, 2)
def test_rope_dype_no_scaling(self):
"""When target is same as base, output should match base rope."""
pos = torch.arange(16).unsqueeze(0).float()
dim = 32
theta = 10000
config = DyPEConfig(base_resolution=1024)
# No scaling needed
result_no_scale = rope_dype(
pos=pos,
dim=dim,
theta=theta,
current_sigma=0.5,
target_height=1024,
target_width=1024,
dype_config=config,
)
# With scaling
result_with_scale = rope_dype(
pos=pos,
dim=dim,
theta=theta,
current_sigma=0.5,
target_height=2048,
target_width=2048,
dype_config=config,
)
# Results should be different when scaling is applied
assert not torch.allclose(result_no_scale, result_with_scale)
def test_rope_dype_late_stage_moves_toward_base_rope(self):
"""Late-stage DyPE should be closer to base RoPE than early-stage DyPE."""
pos = torch.arange(16).unsqueeze(0).float()
dim = 32
theta = 10000
config = DyPEConfig(base_resolution=1024)
base_result = rope_dype(
pos=pos,
dim=dim,
theta=theta,
current_sigma=1.0,
target_height=1024,
target_width=1024,
dype_config=config,
)
early_result = rope_dype(
pos=pos,
dim=dim,
theta=theta,
current_sigma=1.0,
target_height=2048,
target_width=2048,
dype_config=config,
)
late_result = rope_dype(
pos=pos,
dim=dim,
theta=theta,
current_sigma=0.05,
target_height=2048,
target_width=2048,
dype_config=config,
)
early_delta = torch.mean(torch.abs(early_result - base_result))
late_delta = torch.mean(torch.abs(late_result - base_result))
assert late_delta < early_delta
class TestDyPEEmbedND:
"""Tests for DyPEEmbedND module."""
def test_init(self):
"""Test DyPEEmbedND initialization."""
config = DyPEConfig()
embedder = DyPEEmbedND(
dim=128,
theta=10000,
axes_dim=[16, 56, 56],
dype_config=config,
)
assert embedder.dim == 128
assert embedder.theta == 10000
assert embedder.axes_dim == [16, 56, 56]
def test_set_step_state(self):
"""Test step state update."""
config = DyPEConfig()
embedder = DyPEEmbedND(
dim=128,
theta=10000,
axes_dim=[16, 56, 56],
dype_config=config,
)
embedder.set_step_state(sigma=0.5, height=2048, width=2048)
assert embedder._current_sigma == 0.5
assert embedder._target_height == 2048
assert embedder._target_width == 2048
def test_forward_shape(self):
"""Test forward pass output shape."""
config = DyPEConfig()
embedder = DyPEEmbedND(
dim=128,
theta=10000,
axes_dim=[16, 56, 56],
dype_config=config,
)
# Create input ids tensor (batch=1, seq_len=64, n_axes=3)
ids = torch.zeros(1, 64, 3)
result = embedder(ids)
# Output should have shape (batch, 1, seq_len, dim)
# Actually the shape is (batch, 1, seq_len, dim/2, 2, 2) based on rope output
assert result.dim() == 6
assert result.shape[0] == 1 # batch
assert result.shape[1] == 1 # unsqueeze
assert result.shape[2] == 64 # seq_len
class TestDyPEPresets:
"""Tests for DyPE preset configurations."""
def test_preset_4k_exists(self):
"""Test that 4K preset is defined."""
assert DYPE_PRESET_4K in DYPE_PRESETS
def test_get_dype_config_for_resolution_below_threshold(self):
"""When resolution is below threshold, should return None."""
config = get_dype_config_for_resolution(
width=1024,
height=1024,
activation_threshold=1536,
)
assert config is None
config = get_dype_config_for_resolution(
width=1536,
height=1024,
activation_threshold=1536,
)
assert config is None
def test_get_dype_config_for_resolution_above_threshold(self):
"""When resolution is above threshold, should return config."""
config = get_dype_config_for_resolution(
width=2048,
height=2048,
activation_threshold=1536,
)
assert config is not None
assert config.enable_dype is True
def test_get_dype_config_for_resolution_dynamic_scale(self):
"""Higher resolution should result in higher dype_scale."""
config_2k = get_dype_config_for_resolution(
width=2048,
height=2048,
base_resolution=1024,
activation_threshold=1536,
)
config_4k = get_dype_config_for_resolution(
width=4096,
height=4096,
base_resolution=1024,
activation_threshold=1536,
)
assert config_2k is not None
assert config_4k is not None
assert config_4k.dype_scale > config_2k.dype_scale
def test_get_dype_config_for_area_below_threshold(self):
"""When area is below threshold area, should return None."""
config = get_dype_config_for_area(
width=1024,
height=1024,
)
assert config is None
def test_get_dype_config_for_area_above_threshold(self):
"""When area is above threshold area, should return config."""
config = get_dype_config_for_area(
width=2048,
height=1536,
base_resolution=1024,
)
assert config is not None
assert config.enable_dype is True
def test_get_dype_config_for_area_penalizes_extreme_aspect_ratios(self):
balanced_extreme = get_dype_config_for_area(
width=2304,
height=1152,
base_resolution=1024,
)
extreme = get_dype_config_for_area(
width=2304,
height=960,
base_resolution=1024,
)
balanced_same_area = get_dype_config_for_area(
width=2048,
height=1080,
base_resolution=1024,
)
assert balanced_extreme is not None
assert extreme is not None
assert balanced_same_area is not None
assert extreme.dype_scale < balanced_extreme.dype_scale
assert extreme.dype_scale < balanced_same_area.dype_scale
def test_get_dype_config_for_area_is_closer_to_auto_strength(self):
area = get_dype_config_for_area(
width=1728,
height=1152,
base_resolution=1024,
)
auto = get_dype_config_for_resolution(
width=1728,
height=1152,
base_resolution=1024,
activation_threshold=1536,
)
assert area is not None
assert auto is not None
assert area.dype_scale > auto.dype_scale * 0.9
assert area.dype_scale < auto.dype_scale * 1.1
def test_get_dype_config_for_area_uses_higher_exponent_than_old_curve(self):
config = get_dype_config_for_area(
width=1536,
height=1024,
base_resolution=1024,
)
assert config is not None
assert 1.25 <= config.dype_exponent <= 2.0
def test_get_dype_config_from_preset_area(self):
"""Preset AREA should use area-based config."""
config = get_dype_config_from_preset(
preset=DYPE_PRESET_AREA,
width=2048,
height=1536,
)
assert config is not None
assert config.enable_dype is True
def test_get_dype_config_from_preset_off(self):
"""Preset OFF should return None."""
config = get_dype_config_from_preset(
preset=DYPE_PRESET_OFF,
width=2048,
height=2048,
)
assert config is None
def test_get_dype_config_from_preset_auto(self):
"""Preset AUTO should use resolution-based config."""
config = get_dype_config_from_preset(
preset=DYPE_PRESET_AUTO,
width=2048,
height=2048,
)
assert config is not None
assert config.enable_dype is True
def test_get_dype_config_from_preset_4k(self):
"""Preset 4K should use 4K settings."""
config = get_dype_config_from_preset(
preset=DYPE_PRESET_4K,
width=3840,
height=2160,
)
assert config is not None
assert config.enable_dype is True
def test_get_dype_config_from_preset_manual_custom_overrides(self):
"""Custom scale/exponent should override defaults only with 'manual' preset."""
config = get_dype_config_from_preset(
preset=DYPE_PRESET_MANUAL,
width=2048,
height=2048,
custom_scale=5.0,
custom_exponent=10.0,
)
assert config is not None
assert config.dype_scale == 5.0
assert config.dype_exponent == 10.0
def test_get_dype_config_from_preset_4k_ignores_custom(self):
"""4K preset should ignore custom scale/exponent values."""
config = get_dype_config_from_preset(
preset=DYPE_PRESET_4K,
width=3840,
height=2160,
custom_scale=5.0,
custom_exponent=10.0,
)
assert config is not None
# Custom values should be ignored - preset values used instead
assert config.dype_scale == 2.0 # 4K preset default
assert config.dype_exponent == 2.0 # 4K preset default
class TestFrequencyComputation:
"""Tests for frequency computation functions."""
def test_compute_vision_yarn_freqs_shape(self):
"""Test vision_yarn frequency computation shape."""
pos = torch.arange(16).unsqueeze(0).float()
config = DyPEConfig()
cos, sin = compute_vision_yarn_freqs(
pos=pos,
dim=32,
theta=10000,
scale_h=2.0,
scale_w=2.0,
current_sigma=0.5,
dype_config=config,
)
assert cos.shape == sin.shape
assert cos.shape[0] == 1 # batch
assert cos.shape[1] == 16 # seq_len
def test_compute_vision_yarn_freqs_reverts_to_base_rope_at_zero_sigma(self):
pos = torch.arange(16).unsqueeze(0).float()
config = DyPEConfig()
dy_cos, dy_sin = compute_vision_yarn_freqs(
pos=pos,
dim=32,
theta=10000,
scale_h=2.0,
scale_w=2.0,
current_sigma=0.0,
dype_config=config,
)
base_cos, base_sin = compute_vision_yarn_freqs(
pos=pos,
dim=32,
theta=10000,
scale_h=1.0,
scale_w=1.0,
current_sigma=0.0,
dype_config=config,
)
assert torch.allclose(dy_cos, base_cos)
assert torch.allclose(dy_sin, base_sin)
@@ -0,0 +1,78 @@
import sys
import accelerate
import pytest
import torch
from invokeai.backend.flux.ip_adapter.state_dict_utils import (
infer_xlabs_ip_adapter_params_from_state_dict,
is_state_dict_xlabs_ip_adapter,
)
from invokeai.backend.flux.ip_adapter.xlabs_ip_adapter_flux import (
XlabsIpAdapterFlux,
XlabsIpAdapterParams,
)
from tests.backend.flux.ip_adapter.xlabs_flux_ip_adapter_state_dict import xlabs_flux_ip_adapter_sd_shapes
from tests.backend.flux.ip_adapter.xlabs_flux_ip_adapter_v2_state_dict import xlabs_flux_ip_adapter_v2_sd_shapes
@pytest.mark.parametrize("sd_shapes", [xlabs_flux_ip_adapter_sd_shapes, xlabs_flux_ip_adapter_v2_sd_shapes])
def test_is_state_dict_xlabs_ip_adapter(sd_shapes: dict[str, list[int]]):
# Construct a dummy state_dict.
sd = dict.fromkeys(sd_shapes)
assert is_state_dict_xlabs_ip_adapter(sd)
@pytest.mark.skipif(sys.platform == "darwin", reason="Skipping on macOS")
@pytest.mark.parametrize(
["sd_shapes", "expected_params"],
[
(
xlabs_flux_ip_adapter_sd_shapes,
XlabsIpAdapterParams(
num_double_blocks=19,
context_dim=4096,
hidden_dim=3072,
clip_embeddings_dim=768,
clip_extra_context_tokens=4,
),
),
(
xlabs_flux_ip_adapter_v2_sd_shapes,
XlabsIpAdapterParams(
num_double_blocks=19,
context_dim=4096,
hidden_dim=3072,
clip_embeddings_dim=768,
clip_extra_context_tokens=16,
),
),
],
)
def test_infer_xlabs_ip_adapter_params_from_state_dict(
sd_shapes: dict[str, list[int]], expected_params: XlabsIpAdapterParams
):
# Construct a dummy state_dict with tensors of the correct shape on the meta device.
with torch.device("meta"):
sd = {k: torch.zeros(v) for k, v in sd_shapes.items()}
params = infer_xlabs_ip_adapter_params_from_state_dict(sd)
assert params == expected_params
@pytest.mark.skipif(sys.platform == "darwin", reason="Skipping on macOS")
@pytest.mark.parametrize("sd_shapes", [xlabs_flux_ip_adapter_sd_shapes, xlabs_flux_ip_adapter_v2_sd_shapes])
def test_initialize_xlabs_ip_adapter_flux_from_state_dict(sd_shapes: dict[str, list[int]]):
# Construct a dummy state_dict with tensors of the correct shape on the meta device.
with torch.device("meta"):
sd = {k: torch.zeros(v) for k, v in sd_shapes.items()}
# Initialize the XLabs IP-Adapter from the state_dict.
params = infer_xlabs_ip_adapter_params_from_state_dict(sd)
with accelerate.init_empty_weights():
model = XlabsIpAdapterFlux(params=params)
# Smoke test state_dict loading.
model.load_xlabs_state_dict(sd)
@@ -0,0 +1,85 @@
# State dict keys and shapes for an XLabs FLUX IP-Adapter model. Intended to be used for unit tests.
# These keys were extracted from:
# https://huggingface.co/XLabs-AI/flux-ip-adapter/resolve/main/ip_adapter.safetensors
xlabs_flux_ip_adapter_sd_shapes = {
"double_blocks.0.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.0.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.0.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.0.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.1.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.1.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.1.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.1.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.10.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.10.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.10.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.10.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.11.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.11.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.11.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.11.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.12.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.12.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.12.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.12.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.13.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.13.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.13.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.13.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.14.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.14.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.14.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.14.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.15.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.15.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.15.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.15.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.16.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.16.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.16.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.16.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.17.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.17.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.17.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.17.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.18.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.18.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.18.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.18.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.2.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.2.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.2.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.2.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.3.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.3.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.3.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.3.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.4.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.4.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.4.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.4.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.5.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.5.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.5.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.5.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.6.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.6.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.6.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.6.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.7.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.7.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.7.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.7.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.8.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.8.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.8.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.8.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.9.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.9.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.9.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.9.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"ip_adapter_proj_model.norm.bias": [4096],
"ip_adapter_proj_model.norm.weight": [4096],
"ip_adapter_proj_model.proj.bias": [16384],
"ip_adapter_proj_model.proj.weight": [16384, 768],
}
@@ -0,0 +1,85 @@
# State dict keys and shapes for an XLabs FLUX IP-Adapter V2 model. Intended to be used for unit tests.
# These keys were extracted from:
# https://huggingface.co/XLabs-AI/flux-ip-adapter-v2/blob/main/ip_adapter.safetensors
xlabs_flux_ip_adapter_v2_sd_shapes = {
"double_blocks.0.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.0.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.0.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.0.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.1.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.1.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.1.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.1.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.10.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.10.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.10.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.10.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.11.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.11.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.11.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.11.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.12.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.12.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.12.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.12.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.13.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.13.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.13.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.13.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.14.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.14.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.14.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.14.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.15.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.15.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.15.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.15.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.16.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.16.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.16.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.16.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.17.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.17.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.17.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.17.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.18.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.18.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.18.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.18.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.2.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.2.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.2.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.2.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.3.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.3.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.3.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.3.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.4.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.4.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.4.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.4.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.5.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.5.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.5.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.5.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.6.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.6.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.6.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.6.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.7.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.7.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.7.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.7.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.8.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.8.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.8.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.8.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"double_blocks.9.processor.ip_adapter_double_stream_k_proj.bias": [3072],
"double_blocks.9.processor.ip_adapter_double_stream_k_proj.weight": [3072, 4096],
"double_blocks.9.processor.ip_adapter_double_stream_v_proj.bias": [3072],
"double_blocks.9.processor.ip_adapter_double_stream_v_proj.weight": [3072, 4096],
"ip_adapter_proj_model.norm.bias": [4096],
"ip_adapter_proj_model.norm.weight": [4096],
"ip_adapter_proj_model.proj.bias": [65536],
"ip_adapter_proj_model.proj.weight": [65536, 768],
}
@@ -0,0 +1,45 @@
import torch
from invokeai.backend.flux.modules.conditioner import HFEncoder
class FakeTokenizer:
def __call__(
self,
text,
truncation,
max_length,
return_length,
return_overflowing_tokens,
padding,
return_tensors,
):
del text, truncation, max_length, return_length, return_overflowing_tokens, padding, return_tensors
return {"input_ids": torch.tensor([[1, 2, 3]], dtype=torch.long)}
class FakeEncoderOutput(dict):
pass
class FakePartiallyLoadedEncoder(torch.nn.Module):
def __init__(self, effective_device: torch.device):
super().__init__()
self.register_parameter("cpu_param", torch.nn.Parameter(torch.ones(1)))
self.register_buffer("active_buffer", torch.ones(1, device=effective_device))
self.forward_input_device: torch.device | None = None
def forward(self, input_ids: torch.Tensor, attention_mask=None, output_hidden_states: bool = False):
del attention_mask, output_hidden_states
self.forward_input_device = input_ids.device
return FakeEncoderOutput(pooler_output=torch.ones((1, 4), dtype=torch.float32))
def test_hf_encoder_uses_effective_device_for_partially_loaded_models():
effective_device = torch.device("meta")
encoder = FakePartiallyLoadedEncoder(effective_device=effective_device)
hf_encoder = HFEncoder(encoder=encoder, tokenizer=FakeTokenizer(), is_clip=True, max_length=77)
hf_encoder(["test prompt"])
assert encoder.forward_input_device == effective_device
@@ -0,0 +1,28 @@
# The state dict keys and shapes for a FLUX Redux model.
# Model source: https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev/blob/1282f955f706b5240161278f2ef261d2a29ad649/flux1-redux-dev.safetensors
# The keys and shapes were extracted with extract_sd_keys_and_shapes.py.
import torch
from invokeai.backend.flux.redux.flux_redux_state_dict_utils import is_state_dict_likely_flux_redux
from tests.backend.patches.lora_conversions.lora_state_dicts.utils import keys_to_mock_state_dict
flux_redux_keys_and_shapes = {
"redux_down.bias": [4096],
"redux_down.weight": [4096, 12288],
"redux_up.bias": [12288],
"redux_up.weight": [12288, 1152],
}
def test_is_state_dict_likely_flux_redux_true():
# Expand flux_redux_keys_and_shapes to a mock state dict.
sd = keys_to_mock_state_dict(flux_redux_keys_and_shapes)
assert is_state_dict_likely_flux_redux(sd)
def test_is_state_dict_likely_flux_redux_extra_key():
# Expand flux_redux_keys_and_shapes to a mock state dict.
sd = keys_to_mock_state_dict(flux_redux_keys_and_shapes)
# Add an extra key to the state dict.
sd["extra_key"] = torch.rand(1)
assert not is_state_dict_likely_flux_redux(sd)
+319
View File
@@ -0,0 +1,319 @@
"""Tests for Anima scheduler registry."""
import typing
import pytest
from diffusers.schedulers.scheduling_utils import SchedulerMixin
from invokeai.backend.flux.schedulers import (
ANIMA_SCHEDULER_LABELS,
ANIMA_SCHEDULER_MAP,
ANIMA_SCHEDULER_NAME_VALUES,
)
def test_anima_scheduler_map_entries_are_class_kwargs_tuples():
"""Every entry must be (SchedulerClass, kwargs_dict)."""
for name, entry in ANIMA_SCHEDULER_MAP.items():
assert isinstance(entry, tuple), f"{name} is not a tuple"
assert len(entry) == 2, f"{name} tuple has wrong arity"
cls, kwargs = entry
assert isinstance(cls, type) and issubclass(cls, SchedulerMixin), (
f"{name} first element is not a SchedulerMixin subclass"
)
assert isinstance(kwargs, dict), f"{name} second element is not a dict"
def test_anima_scheduler_map_entries_can_be_constructed():
"""Every entry must construct cleanly by splatting its kwargs."""
for name, (cls, kwargs) in ANIMA_SCHEDULER_MAP.items():
scheduler = cls(num_train_timesteps=1000, **kwargs)
assert isinstance(scheduler, SchedulerMixin), f"{name} did not produce a SchedulerMixin"
def test_anima_scheduler_labels_cover_every_map_key():
for name in ANIMA_SCHEDULER_MAP.keys():
assert name in ANIMA_SCHEDULER_LABELS, f"{name} has no label"
def test_anima_scheduler_map_includes_new_dpmpp_entries():
assert "dpmpp_2m" in ANIMA_SCHEDULER_MAP
assert "dpmpp_2m_sde" in ANIMA_SCHEDULER_MAP
def test_anima_dpmpp_2m_uses_flow_prediction():
from invokeai.backend.flux.schedulers import ANIMA_SHIFT
cls, kwargs = ANIMA_SCHEDULER_MAP["dpmpp_2m"]
assert kwargs["prediction_type"] == "flow_prediction"
assert kwargs["use_flow_sigmas"] is True
assert kwargs["flow_shift"] == ANIMA_SHIFT
assert kwargs["solver_order"] == 2
assert "algorithm_type" not in kwargs # deterministic, default algorithm
def test_anima_dpmpp_2m_sde_uses_sde_algorithm():
from invokeai.backend.flux.schedulers import ANIMA_SHIFT
cls, kwargs = ANIMA_SCHEDULER_MAP["dpmpp_2m_sde"]
assert kwargs["prediction_type"] == "flow_prediction"
assert kwargs["use_flow_sigmas"] is True
assert kwargs["flow_shift"] == ANIMA_SHIFT
assert kwargs["algorithm_type"] == "sde-dpmsolver++"
assert kwargs["solver_order"] == 2
def test_anima_dpmpp_2m_produces_anima_compatible_sigma_schedule():
"""The DPM++ 2M scheduler, when run through the same dispatch logic as
anima_denoise._run_diffusion, must produce a sigma schedule equivalent
to Anima's reference schedule (loglinear_timestep_shift with shift=3.0).
On diffusers 0.35.1, DPMSolverMultistepScheduler.set_timesteps does not
accept `sigmas=`, so the runtime falls back to num_inference_steps and
relies on the scheduler's internal flow_shift=3.0 to compute equivalent
sigmas. This test verifies that equivalence end-to-end.
"""
import inspect
from invokeai.app.invocations.anima_denoise import loglinear_timestep_shift
from invokeai.backend.flux.schedulers import ANIMA_SHIFT
num_steps = 10
# Reference: Anima's own pre-shifted sigma schedule.
anima_sigmas = [loglinear_timestep_shift(ANIMA_SHIFT, 1.0 - i / num_steps) for i in range(num_steps + 1)]
cls, kwargs = ANIMA_SCHEDULER_MAP["dpmpp_2m"]
scheduler = cls(num_train_timesteps=1000, **kwargs)
# Mirror anima_denoise.py:502-506 dispatch.
sig = inspect.signature(scheduler.set_timesteps)
if "sigmas" in sig.parameters:
scheduler.set_timesteps(sigmas=anima_sigmas, device="cpu")
else:
scheduler.set_timesteps(num_inference_steps=num_steps, device="cpu")
diffusers_sigmas = [float(s) for s in scheduler.sigmas[: len(anima_sigmas)]]
max_diff = max(abs(a - b) for a, b in zip(anima_sigmas, diffusers_sigmas, strict=True))
assert max_diff < 1e-3, f"DPM++ 2M sigma schedule diverges from Anima reference (max abs diff = {max_diff:.6f})"
def test_anima_dpmpp_2m_with_denoising_start_honors_clipped_schedule():
"""DPM++ img2img: the set_begin_index path must start at the correct sigma.
When DPMSolverMultistepScheduler doesn't accept sigmas=, anima_denoise falls back to
set_timesteps(num_inference_steps=full_steps) + set_begin_index(start_idx). The
effective first sigma must match the clipped Anima reference schedule within 1e-3.
"""
import inspect
from invokeai.app.invocations.anima_denoise import loglinear_timestep_shift
from invokeai.backend.flux.schedulers import ANIMA_SHIFT
num_steps = 30
denoising_start = 0.5
start_idx = int(denoising_start * num_steps) # mirrors anima_denoise clipping math
full_sigmas = [loglinear_timestep_shift(ANIMA_SHIFT, 1.0 - i / num_steps) for i in range(num_steps + 1)]
expected_first_sigma = full_sigmas[start_idx]
cls, kwargs = ANIMA_SCHEDULER_MAP["dpmpp_2m"]
scheduler = cls(num_train_timesteps=1000, **kwargs)
sig = inspect.signature(scheduler.set_timesteps)
if "sigmas" in sig.parameters:
# Future diffusers: sigmas= supported, clipped schedule passed directly.
scheduler.set_timesteps(sigmas=full_sigmas[start_idx:], device="cpu")
actual_first_sigma = float(scheduler.sigmas[0])
else:
# Current diffusers: use set_begin_index on the full schedule.
scheduler.set_timesteps(num_inference_steps=num_steps, device="cpu")
scheduler.set_begin_index(start_idx)
actual_first_sigma = float(scheduler.sigmas[start_idx])
assert abs(actual_first_sigma - expected_first_sigma) < 1e-3, (
f"DPM++ first sigma with denoising_start=0.5: got {actual_first_sigma:.6f}, expected {expected_first_sigma:.6f}"
)
def test_anima_set_begin_index_path_step_count_with_denoising_end():
"""set_begin_index fallback must honour denoising_end, not just denoising_start.
Regression test: the old formula (len(timesteps) - begin_index) ignored denoising_end
and ran past it. For steps=30, denoising_start=0.2, denoising_end=0.8 the correct
step count is 18, not 24.
"""
import inspect
from invokeai.app.invocations.anima_denoise import loglinear_timestep_shift
from invokeai.backend.flux.schedulers import ANIMA_SHIFT
num_steps = 30
denoising_start = 0.2
denoising_end = 0.8
# Reference step count from the Euler path (clipped sigmas).
full_sigmas = [loglinear_timestep_shift(ANIMA_SHIFT, 1.0 - i / num_steps) for i in range(num_steps + 1)]
total_sigmas = len(full_sigmas)
start_idx = int(denoising_start * (total_sigmas - 1))
end_idx = int(denoising_end * (total_sigmas - 1)) + 1
expected_steps = (end_idx - start_idx) - 1 # 18
cls, kwargs = ANIMA_SCHEDULER_MAP["dpmpp_2m"]
scheduler = cls(num_train_timesteps=1000, **kwargs)
sig = inspect.signature(scheduler.set_timesteps)
scheduler_begin_index = int(denoising_start * num_steps)
if "sigmas" in sig.parameters:
clipped = full_sigmas[start_idx:end_idx]
scheduler.set_timesteps(sigmas=clipped, device="cpu")
num_scheduler_steps = len(scheduler.timesteps)
else:
scheduler.set_timesteps(num_inference_steps=num_steps, device="cpu")
scheduler.set_begin_index(scheduler_begin_index)
num_scheduler_steps = int(denoising_end * num_steps) - scheduler_begin_index
assert num_scheduler_steps == expected_steps, (
f"DPM++ scheduler step count with denoising_start={denoising_start}, "
f"denoising_end={denoising_end}: got {num_scheduler_steps}, expected {expected_steps}"
)
@pytest.mark.parametrize(
["denoising_start", "denoising_end", "steps"],
[
(0.2, 0.8, 30), # mid-range: 18 logical steps → 36 doubled calls
(0.0, 0.8, 30), # start-only clip: 24 logical steps → 48 doubled calls
(0.2, 1.0, 30), # end=1.0: clamp kicks in (last step first-order only)
(0.5, 0.75, 20), # different step count
],
)
def test_anima_heun_set_begin_index_path_begin_index_and_step_count(
denoising_start: float, denoising_end: float, steps: int
):
"""Heun img2img: set_begin_index must use doubled-array index and step count must
account for Heun's 2N-1 timestep structure.
Logical step k maps to doubled-array begin index 2k. For a range [k_start, k_end)
the total calls is 2*(k_end-k_start), clamped to len(timesteps)-begin_index so that
denoising_end=1.0 correctly gets the 2N-1 (not 2N) count.
"""
from diffusers import FlowMatchHeunDiscreteScheduler
k_start = int(denoising_start * steps)
k_end = int(denoising_end * steps)
scheduler = FlowMatchHeunDiscreteScheduler(num_train_timesteps=1000, shift=1.0)
scheduler.set_timesteps(num_inference_steps=steps, device="cpu")
expected_begin_index = 2 * k_start
expected_steps = min(2 * (k_end - k_start), len(scheduler.timesteps) - expected_begin_index)
# Verify the doubled structure: len(timesteps) == 2*steps - 1
assert len(scheduler.timesteps) == 2 * steps - 1, (
f"Heun timesteps length: expected {2 * steps - 1}, got {len(scheduler.timesteps)}"
)
# The fixed code's begin index must map logical step to doubled-array space.
assert expected_begin_index == 2 * k_start
# For mid-range (denoising_end < 1): all steps in range have first + second order.
if denoising_end < 1.0:
assert expected_steps == 2 * (k_end - k_start), (
f"mid-range step count: expected {2 * (k_end - k_start)}, got {expected_steps}"
)
# For denoising_end=1.0: last step is first-order only → clamped to 2N-1-begin.
if denoising_end == 1.0 and k_start > 0:
full_from_begin = len(scheduler.timesteps) - expected_begin_index
assert expected_steps == full_from_begin
# Bounds check: begin_index + num_steps must not exceed len(timesteps).
assert expected_begin_index + expected_steps <= len(scheduler.timesteps), (
f"step range [{expected_begin_index}, {expected_begin_index + expected_steps}) "
f"exceeds timesteps length {len(scheduler.timesteps)}"
)
# Sigma sanity: the sigma at the doubled begin index must equal the sigma at logical k_start.
# sigmas has 2N entries; sigmas[2k] == s_k for all k.
assert len(scheduler.sigmas) == 2 * steps
sigma_at_begin = scheduler.sigmas[expected_begin_index].item()
sigma_at_logical_k = scheduler.sigmas[2 * k_start].item()
assert abs(sigma_at_begin - sigma_at_logical_k) < 1e-6
def test_anima_literal_covers_every_map_key():
"""Catch the silent failure mode where a new entry lands in the map but
the Literal isn't updated — Pydantic validation would still accept it
via runtime introspection but type-check tooling would not."""
literal_values = set(typing.get_args(ANIMA_SCHEDULER_NAME_VALUES))
for name in ANIMA_SCHEDULER_MAP:
assert name in literal_values, f"{name} is in the map but missing from the Literal"
def test_anima_scheduler_literal_includes_er_sde():
"""er_sde must appear in the literal type, have a label, and be
registered in ANIMA_SCHEDULER_MAP for dispatch through the universal
scheduler path."""
literal_args = typing.get_args(ANIMA_SCHEDULER_NAME_VALUES)
assert "er_sde" in literal_args
assert "er_sde" in ANIMA_SCHEDULER_LABELS
assert ANIMA_SCHEDULER_LABELS["er_sde"] == "ER-SDE"
assert "er_sde" in ANIMA_SCHEDULER_MAP
def test_anima_heun_uses_anima_shift_for_internal_schedule():
"""Heun does NOT accept set_timesteps(sigmas=...) so it always builds its own internal
schedule. With shift=1.0 (the previous setting), that schedule was linear and gave the
wrong noise levels for img2img — Heun's sigmas[2*k_start] would be ~0.48 when Anima's
reference at user step k_start (denoising_start=0.5) is ~0.75. The model would receive
a timestep matching neither the latents nor its training distribution.
Fix: give Heun shift=ANIMA_SHIFT so its internal schedule approximates Anima's reference.
"""
from invokeai.app.invocations.anima_denoise import loglinear_timestep_shift
cls, kwargs = ANIMA_SCHEDULER_MAP["heun"]
from invokeai.backend.flux.schedulers import ANIMA_SHIFT
assert kwargs["shift"] == ANIMA_SHIFT, (
f"Heun must use shift={ANIMA_SHIFT} (Anima's loglinear shift) since it doesn't accept "
f"sigmas=; got shift={kwargs['shift']}"
)
# Verify the schedule approximates Anima's reference for the bulk of user steps.
# The two formulas diverge at the tail (Heun uses linspace(1, T, N+1), Anima uses
# 1 - i/N), so we tolerate up to 5% absolute. The previous shift=1.0 bug gave 25-40%+
# divergence at mid-schedule, so any reasonable tolerance catches that regression.
steps = 30
anima_ref = [loglinear_timestep_shift(ANIMA_SHIFT, 1.0 - i / steps) for i in range(steps + 1)]
s = cls(num_train_timesteps=1000, **kwargs)
s.set_timesteps(num_inference_steps=steps, device="cpu")
# Heun's sigmas array has 2*N entries; sigmas[2*k] is the noise level at user step k.
for k in (0, 5, 10, 15):
heun_sigma = s.sigmas[2 * k].item()
ref = anima_ref[k]
assert abs(heun_sigma - ref) < 0.05, (
f"Heun internal sigma at user step {k} ({heun_sigma:.4f}) diverges from "
f"Anima reference ({ref:.4f}) by more than 5% — likely a shift kwarg regression"
)
def test_anima_scheduler_map_er_sde_entry():
"""ANIMA_SCHEDULER_MAP['er_sde'] must map to ERSDEScheduler with rectified-flow kwargs.
This is the wiring that lets Anima dispatch er_sde through the universal scheduler
path (replacing the legacy elif is_er_sde: branch in anima_denoise.py).
"""
from invokeai.backend.flux.schedulers import ANIMA_SHIFT
from invokeai.backend.rectified_flow.er_sde_scheduler import ERSDEScheduler
assert "er_sde" in ANIMA_SCHEDULER_MAP, "er_sde must be in ANIMA_SCHEDULER_MAP"
cls, kwargs = ANIMA_SCHEDULER_MAP["er_sde"]
assert cls is ERSDEScheduler
assert kwargs["use_flow_sigmas"] is True
assert kwargs["prediction_type"] == "flow_prediction"
assert kwargs["solver_order"] == 3
assert kwargs["stochastic"] is True
assert kwargs["flow_shift"] == ANIMA_SHIFT
+249
View File
@@ -0,0 +1,249 @@
from types import SimpleNamespace
import pytest
import torch
from invokeai.backend.flux.denoise import denoise
from invokeai.backend.flux.schedulers import FLUX_SCHEDULER_MAP
class _FakeFluxModel:
def __call__(
self,
img: torch.Tensor,
img_ids: torch.Tensor,
txt: torch.Tensor,
txt_ids: torch.Tensor,
y: torch.Tensor,
timesteps: torch.Tensor,
guidance: torch.Tensor,
timestep_index: int,
total_num_timesteps: int,
controlnet_double_block_residuals: list[torch.Tensor] | None,
controlnet_single_block_residuals: list[torch.Tensor] | None,
ip_adapter_extensions: list[object],
regional_prompting_extension: object,
) -> torch.Tensor:
return torch.zeros_like(img)
class _FakeDyPEExtension:
def __init__(self) -> None:
self.sigmas: list[float] = []
def patch_model(self, model: object) -> tuple[object, None]:
return object(), None
def update_step_state(self, embedder: object, sigma: float) -> None:
self.sigmas.append(sigma)
class _FakeScheduler:
def __init__(self) -> None:
self.config = SimpleNamespace(num_train_timesteps=1000)
self.timesteps = torch.tensor([], dtype=torch.float32)
self.sigmas = torch.tensor([], dtype=torch.float32)
def set_timesteps(self, sigmas: list[float], device: torch.device) -> None:
del device
self.sigmas = torch.tensor(sigmas, dtype=torch.float32)
self.timesteps = torch.tensor([900.0, 400.0], dtype=torch.float32)
def step(self, model_output: torch.Tensor, timestep: torch.Tensor, sample: torch.Tensor) -> SimpleNamespace:
del model_output, timestep
return SimpleNamespace(prev_sample=sample)
class _FakeHeunScheduler:
def __init__(self) -> None:
self.config = SimpleNamespace(num_train_timesteps=1000)
self.timesteps = torch.tensor([], dtype=torch.float32)
self.sigmas = torch.tensor([], dtype=torch.float32)
self.state_in_first_order = True
self._step_index = 0
def set_timesteps(self, sigmas: list[float], device: torch.device) -> None:
del device
# Duplicate each user-facing step to mimic a second-order scheduler.
self.sigmas = torch.tensor([1.0, 1.0, 0.25, 0.25, 0.0], dtype=torch.float32)
self.timesteps = torch.tensor([900.0, 850.0, 400.0, 350.0], dtype=torch.float32)
self._step_index = 0
self.state_in_first_order = True
def step(self, model_output: torch.Tensor, timestep: torch.Tensor, sample: torch.Tensor) -> SimpleNamespace:
del model_output, timestep
self._step_index += 1
self.state_in_first_order = self._step_index % 2 == 0
return SimpleNamespace(prev_sample=sample)
class _FakePbar:
def update(self, value: int) -> None:
del value
def close(self) -> None:
return None
def _fake_tqdm(iterable=None, **kwargs):
del kwargs
if iterable is None:
return _FakePbar()
return iterable
def _build_regional_prompting_extension(batch_size: int) -> SimpleNamespace:
return SimpleNamespace(
regional_text_conditioning=SimpleNamespace(
t5_embeddings=torch.zeros(batch_size, 1, 4),
t5_txt_ids=torch.zeros(batch_size, 1, 3),
clip_embeddings=torch.zeros(batch_size, 4),
)
)
def test_denoise_euler_path_updates_dype_with_sigma(monkeypatch):
monkeypatch.setattr("invokeai.backend.flux.denoise.tqdm", _fake_tqdm)
model = _FakeFluxModel()
dype_extension = _FakeDyPEExtension()
img = torch.zeros(1, 2, 4)
img_ids = torch.zeros(1, 2, 3)
regional_prompting_extension = _build_regional_prompting_extension(batch_size=1)
callback_steps: list[int] = []
result = denoise(
model=model,
img=img,
img_ids=img_ids,
pos_regional_prompting_extension=regional_prompting_extension,
neg_regional_prompting_extension=None,
timesteps=[1.0, 0.5, 0.0],
step_callback=lambda state: callback_steps.append(state.step),
guidance=1.0,
cfg_scale=[1.0, 1.0],
inpaint_extension=None,
controlnet_extensions=[],
pos_ip_adapter_extensions=[],
neg_ip_adapter_extensions=[],
img_cond=None,
img_cond_seq=None,
img_cond_seq_ids=None,
dype_extension=dype_extension,
scheduler=None,
)
assert torch.equal(result, img)
assert dype_extension.sigmas == [1.0, 0.5]
assert callback_steps == [1, 2]
def test_denoise_scheduler_path_prefers_scheduler_sigmas_for_dype(monkeypatch):
monkeypatch.setattr("invokeai.backend.flux.denoise.tqdm", _fake_tqdm)
model = _FakeFluxModel()
scheduler = _FakeScheduler()
dype_extension = _FakeDyPEExtension()
img = torch.zeros(1, 2, 4)
img_ids = torch.zeros(1, 2, 3)
regional_prompting_extension = _build_regional_prompting_extension(batch_size=1)
denoise(
model=model,
img=img,
img_ids=img_ids,
pos_regional_prompting_extension=regional_prompting_extension,
neg_regional_prompting_extension=None,
timesteps=[1.0, 0.25, 0.0],
step_callback=lambda state: None,
guidance=1.0,
cfg_scale=[1.0, 1.0],
inpaint_extension=None,
controlnet_extensions=[],
pos_ip_adapter_extensions=[],
neg_ip_adapter_extensions=[],
img_cond=None,
img_cond_seq=None,
img_cond_seq_ids=None,
dype_extension=dype_extension,
scheduler=scheduler,
)
# Scheduler timesteps normalize to [0.9, 0.4], so this asserts the scheduler
# sigma sequence is what DyPE actually consumes.
assert dype_extension.sigmas == [1.0, 0.25]
def test_denoise_heun_scheduler_path_uses_internal_scheduler_sigmas(monkeypatch):
monkeypatch.setattr("invokeai.backend.flux.denoise.tqdm", _fake_tqdm)
model = _FakeFluxModel()
scheduler = _FakeHeunScheduler()
dype_extension = _FakeDyPEExtension()
img = torch.zeros(1, 2, 4)
img_ids = torch.zeros(1, 2, 3)
regional_prompting_extension = _build_regional_prompting_extension(batch_size=1)
callback_steps: list[int] = []
denoise(
model=model,
img=img,
img_ids=img_ids,
pos_regional_prompting_extension=regional_prompting_extension,
neg_regional_prompting_extension=None,
timesteps=[1.0, 0.25, 0.0],
step_callback=lambda state: callback_steps.append(state.step),
guidance=1.0,
cfg_scale=[1.0, 1.0],
inpaint_extension=None,
controlnet_extensions=[],
pos_ip_adapter_extensions=[],
neg_ip_adapter_extensions=[],
img_cond=None,
img_cond_seq=None,
img_cond_seq_ids=None,
dype_extension=dype_extension,
scheduler=scheduler,
)
assert dype_extension.sigmas == [1.0, 1.0, 0.25, 0.25]
assert callback_steps == [1, 2]
@pytest.mark.parametrize("scheduler_name", sorted(FLUX_SCHEDULER_MAP))
def test_denoise_real_flux_schedulers_update_dype_from_internal_sigma_schedule(monkeypatch, scheduler_name):
monkeypatch.setattr("invokeai.backend.flux.denoise.tqdm", _fake_tqdm)
model = _FakeFluxModel()
scheduler = FLUX_SCHEDULER_MAP[scheduler_name](num_train_timesteps=1000)
dype_extension = _FakeDyPEExtension()
img = torch.zeros(1, 2, 4)
img_ids = torch.zeros(1, 2, 3)
regional_prompting_extension = _build_regional_prompting_extension(batch_size=1)
callback_steps: list[int] = []
denoise(
model=model,
img=img,
img_ids=img_ids,
pos_regional_prompting_extension=regional_prompting_extension,
neg_regional_prompting_extension=None,
timesteps=[1.0, 0.25, 0.0],
step_callback=lambda state: callback_steps.append(state.step),
guidance=1.0,
cfg_scale=[1.0, 1.0],
inpaint_extension=None,
controlnet_extensions=[],
pos_ip_adapter_extensions=[],
neg_ip_adapter_extensions=[],
img_cond=None,
img_cond_seq=None,
img_cond_seq_ids=None,
dype_extension=dype_extension,
scheduler=scheduler,
)
assert dype_extension.sigmas
expected_sigmas = [float(sigma) for sigma in scheduler.sigmas[: len(dype_extension.sigmas)]]
assert dype_extension.sigmas == expected_sigmas
assert callback_steps
+76
View File
@@ -0,0 +1,76 @@
import pytest
import torch
from invokeai.backend.flux.sampling_utils import clip_timestep_schedule, clip_timestep_schedule_fractional
def float_lists_almost_equal(list1: list[float], list2: list[float], tol: float = 1e-6) -> bool:
return all(abs(a - b) < tol for a, b in zip(list1, list2, strict=True))
@pytest.mark.parametrize(
["denoising_start", "denoising_end", "expected_timesteps", "raises"],
[
(0.0, 1.0, [1.0, 0.75, 0.5, 0.25, 0.0], False), # Default case.
(-0.1, 1.0, [], True), # Negative denoising_start should raise.
(0.0, 1.1, [], True), # denoising_end > 1 should raise.
(0.5, 0.0, [], True), # denoising_start > denoising_end should raise.
(0.0, 0.0, [1.0], False), # denoising_end == 0.
(1.0, 1.0, [0.0], False), # denoising_start == 1.
(0.2, 0.8, [1.0, 0.75, 0.5, 0.25], False), # Middle of the schedule.
# If we denoise from 0.0 to x, then from x to 1.0, it is important that denoise_end = x and denoise_start = x
# map to the same timestep. We test this first when x is equal to a timestep, then when it falls between two
# timesteps.
# x = 0.5
(0.0, 0.5, [1.0, 0.75, 0.5], False),
(0.5, 1.0, [0.5, 0.25, 0.0], False),
# x = 0.3
(0.0, 0.3, [1.0, 0.75], False),
(0.3, 1.0, [0.75, 0.5, 0.25, 0.0], False),
],
)
def test_clip_timestep_schedule(
denoising_start: float, denoising_end: float, expected_timesteps: list[float], raises: bool
):
timesteps = torch.linspace(1, 0, 5).tolist()
if raises:
with pytest.raises(AssertionError):
clip_timestep_schedule(timesteps, denoising_start, denoising_end)
else:
assert float_lists_almost_equal(
clip_timestep_schedule(timesteps, denoising_start, denoising_end), expected_timesteps
)
@pytest.mark.parametrize(
["denoising_start", "denoising_end", "expected_timesteps", "raises"],
[
(0.0, 1.0, [1.0, 0.75, 0.5, 0.25, 0.0], False), # Default case.
(-0.1, 1.0, [], True), # Negative denoising_start should raise.
(0.0, 1.1, [], True), # denoising_end > 1 should raise.
(0.5, 0.0, [], True), # denoising_start > denoising_end should raise.
(0.0, 0.0, [1.0], False), # denoising_end == 0.
(1.0, 1.0, [0.0], False), # denoising_start == 1.
(0.2, 0.8, [0.8, 0.75, 0.5, 0.25, 0.2], False), # Middle of the schedule.
# If we denoise from 0.0 to x, then from x to 1.0, it is important that denoise_end = x and denoise_start = x
# map to the same timestep. We test this first when x is equal to a timestep, then when it falls between two
# timesteps.
# x = 0.5
(0.0, 0.5, [1.0, 0.75, 0.5], False),
(0.5, 1.0, [0.5, 0.25, 0.0], False),
# x = 0.3
(0.0, 0.3, [1.0, 0.75, 0.7], False),
(0.3, 1.0, [0.7, 0.5, 0.25, 0.0], False),
],
)
def test_clip_timestep_schedule_fractional(
denoising_start: float, denoising_end: float, expected_timesteps: list[float], raises: bool
):
timesteps = torch.linspace(1, 0, 5).tolist()
if raises:
with pytest.raises(AssertionError):
clip_timestep_schedule_fractional(timesteps, denoising_start, denoising_end)
else:
assert float_lists_almost_equal(
clip_timestep_schedule_fractional(timesteps, denoising_start, denoising_end), expected_timesteps
)
@@ -0,0 +1,620 @@
import pytest
import torch
from invokeai.backend.image_util import color_conversion
from invokeai.invocation_api import (
hsl_from_linear_srgb,
hsl_from_srgb,
lab_from_linear_srgb,
lab_from_srgb,
lab_from_xyz,
linear_srgb_from_hsl,
linear_srgb_from_lab,
linear_srgb_from_oklab,
linear_srgb_from_oklch,
linear_srgb_from_srgb,
linear_srgb_from_xyz,
okhsl_from_srgb,
okhsv_from_srgb,
oklab_from_linear_srgb,
oklab_from_oklch,
oklab_from_srgb,
oklab_from_xyz,
oklch_from_linear_srgb,
oklch_from_oklab,
oklch_from_srgb,
oklch_from_xyz,
srgb_from_hsl,
srgb_from_lab,
srgb_from_linear_srgb,
srgb_from_okhsl,
srgb_from_okhsv,
srgb_from_oklab,
srgb_from_oklch,
srgb_from_xyz,
xyz_d50_to_d65,
xyz_d65_to_d50,
xyz_from_lab,
xyz_from_linear_srgb,
xyz_from_oklab,
xyz_from_oklch,
xyz_from_srgb,
)
def test_srgb_oklab_round_trip() -> None:
srgb = torch.tensor(
[
[[0.0, 1.0], [1.0, 0.1]],
[[0.0, 1.0], [0.0, 0.6]],
[[0.0, 1.0], [0.0, 0.9]],
],
dtype=torch.float32,
)
round_tripped = srgb_from_linear_srgb(linear_srgb_from_oklab(oklab_from_linear_srgb(linear_srgb_from_srgb(srgb))))
assert torch.allclose(round_tripped, srgb, atol=1e-5)
def test_oklab_from_srgb_matches_explicit_conversion_path() -> None:
srgb = torch.tensor(
[
[[0.0, 1.0], [1.0, 0.1]],
[[0.0, 1.0], [0.0, 0.6]],
[[0.0, 1.0], [0.0, 0.9]],
],
dtype=torch.float32,
)
direct = oklab_from_srgb(srgb)
via_linear_srgb = oklab_from_linear_srgb(linear_srgb_from_srgb(srgb))
assert torch.allclose(direct, via_linear_srgb, atol=1e-6)
def test_srgb_from_oklab_matches_explicit_conversion_path() -> None:
oklab = torch.tensor(
[
[[0.6, 0.4]],
[[0.2, -0.1]],
[[0.1, 0.05]],
],
dtype=torch.float32,
)
direct = srgb_from_oklab(oklab)
via_linear_srgb = srgb_from_linear_srgb(linear_srgb_from_oklab(oklab))
assert torch.allclose(direct, via_linear_srgb, atol=1e-6)
def test_oklab_from_pure_srgb_red_matches_reference_value() -> None:
srgb_red = torch.tensor([[[1.0]], [[0.0]], [[0.0]]], dtype=torch.float32)
oklab_red = oklab_from_linear_srgb(linear_srgb_from_srgb(srgb_red))
assert torch.allclose(
oklab_red[:, 0, 0],
torch.tensor([0.62795536, 0.22486306, 0.1258463], dtype=torch.float32),
atol=1e-6,
)
def test_oklab_oklch_round_trip() -> None:
oklab = torch.tensor(
[
[[0.6, 0.4]],
[[0.2, -0.1]],
[[0.1, 0.05]],
],
dtype=torch.float32,
)
round_tripped = oklab_from_oklch(oklch_from_oklab(oklab))
assert torch.allclose(round_tripped, oklab, atol=1e-6)
def test_oklch_from_linear_srgb_matches_explicit_conversion_path() -> None:
linear_srgb = torch.tensor(
[
[[0.1, 0.9]],
[[0.4, 0.2]],
[[0.7, 0.3]],
],
dtype=torch.float32,
)
direct = oklch_from_linear_srgb(linear_srgb)
via_oklab = oklch_from_oklab(oklab_from_linear_srgb(linear_srgb))
assert torch.allclose(direct, via_oklab, atol=1e-6)
def test_oklch_from_srgb_and_back_round_trip() -> None:
srgb = torch.tensor(
[
[[0.2, 0.9]],
[[0.4, 0.3]],
[[0.8, 0.1]],
],
dtype=torch.float32,
)
direct_round_trip = srgb_from_oklch(oklch_from_srgb(srgb))
explicit_round_trip = srgb_from_linear_srgb(
linear_srgb_from_oklch(oklch_from_oklab(oklab_from_linear_srgb(linear_srgb_from_srgb(srgb))))
)
assert torch.allclose(direct_round_trip, srgb, atol=1e-5)
assert torch.allclose(explicit_round_trip, srgb, atol=1e-5)
def test_linear_srgb_from_oklch_matches_oklab_path() -> None:
oklch = torch.tensor(
[
[[0.7, 0.5]],
[[0.12, 0.04]],
[[30.0, 210.0]],
],
dtype=torch.float32,
)
direct = linear_srgb_from_oklch(oklch)
via_oklab = linear_srgb_from_oklab(oklab_from_oklch(oklch))
assert torch.allclose(direct, via_oklab, atol=1e-6)
assert direct.shape == (3, 1, 2)
def test_hsl_srgb_round_trip() -> None:
srgb = torch.tensor(
[
[[0.0, 1.0], [0.25, 0.9]],
[[0.2, 0.8], [0.75, 0.1]],
[[1.0, 0.1], [0.5, 0.4]],
],
dtype=torch.float32,
)
round_tripped = srgb_from_hsl(hsl_from_srgb(srgb))
assert torch.allclose(round_tripped, srgb, atol=1e-5)
def test_hsl_hue_is_expressed_in_degrees() -> None:
srgb = torch.tensor(
[
[[1.0, 0.0, 0.0]],
[[0.0, 1.0, 0.0]],
[[0.0, 0.0, 1.0]],
],
dtype=torch.float32,
)
hsl = hsl_from_srgb(srgb)
assert torch.allclose(hsl[0, 0, :], torch.tensor([0.0, 120.0, 240.0]), atol=1e-3)
assert torch.allclose(hsl[1, 0, :], torch.tensor([1.0, 1.0, 1.0]), atol=1e-6)
assert torch.allclose(hsl[2, 0, :], torch.tensor([0.5, 0.5, 0.5]), atol=1e-6)
def test_hsl_from_grayscale_has_zero_saturation() -> None:
srgb = torch.tensor(
[
[[0.1, 0.8]],
[[0.1, 0.8]],
[[0.1, 0.8]],
],
dtype=torch.float32,
)
hsl = hsl_from_srgb(srgb)
assert torch.allclose(hsl[1, ...], torch.zeros_like(hsl[1, ...]), atol=1e-6)
def test_hsl_from_linear_srgb_matches_explicit_conversion_path() -> None:
linear_srgb = torch.tensor(
[
[[0.0, 1.0], [0.25, 0.8]],
[[0.2, 0.8], [0.75, 0.1]],
[[1.0, 0.1], [0.5, 0.4]],
],
dtype=torch.float32,
)
direct = hsl_from_linear_srgb(linear_srgb)
via_srgb = hsl_from_srgb(srgb_from_linear_srgb(linear_srgb))
assert torch.allclose(direct, via_srgb, atol=1e-6)
def test_linear_srgb_from_hsl_matches_explicit_conversion_path() -> None:
hsl = torch.tensor(
[
[[0.0, 216.0], [90.0, 324.0]],
[[1.0, 0.25], [0.75, 0.1]],
[[0.5, 0.4], [0.2, 0.8]],
],
dtype=torch.float32,
)
direct = linear_srgb_from_hsl(hsl)
via_srgb = linear_srgb_from_srgb(srgb_from_hsl(hsl))
assert torch.allclose(direct, via_srgb, atol=1e-6)
def test_srgb_from_hsl_wraps_degree_hue_values() -> None:
hsl = torch.tensor(
[
[[360.0, -120.0]],
[[1.0, 1.0]],
[[0.5, 0.5]],
],
dtype=torch.float32,
)
rgb = srgb_from_hsl(hsl)
assert torch.allclose(rgb[:, 0, 0], torch.tensor([1.0, 0.0, 0.0]), atol=1e-5)
assert torch.allclose(rgb[:, 0, 1], torch.tensor([0.0, 0.0, 1.0]), atol=1e-5)
def test_okhsl_srgb_round_trip() -> None:
srgb = torch.tensor(
[
[[0.05, 0.95], [0.2, 0.8]],
[[0.4, 0.2], [0.6, 0.1]],
[[0.9, 0.05], [0.3, 0.7]],
],
dtype=torch.float32,
)
round_tripped = srgb_from_okhsl(okhsl_from_srgb(srgb))
assert torch.allclose(round_tripped, srgb, atol=5e-4)
def test_okhsl_and_okhsv_hue_are_expressed_in_degrees() -> None:
srgb = torch.tensor(
[
[[1.0, 0.0, 0.0]],
[[0.0, 1.0, 0.0]],
[[0.0, 0.0, 1.0]],
],
dtype=torch.float32,
)
okhsl = okhsl_from_srgb(srgb)
okhsv = okhsv_from_srgb(srgb)
assert torch.allclose(okhsl[0, 0, :], torch.tensor([29.2473, 142.4848, 264.0487]), atol=2e-2)
assert torch.allclose(okhsv[0, 0, :], torch.tensor([29.2473, 142.4848, 264.0487]), atol=2e-2)
assert torch.allclose(okhsl[1, 0, :], torch.tensor([1.0, 1.0, 1.0]), atol=1e-6)
assert torch.allclose(okhsv[1, 0, :], torch.tensor([1.0, 1.0, 1.0]), atol=1e-6)
def test_okhsl_and_okhsv_srgb_round_trip() -> None:
srgb = torch.tensor(
[
[[0.05, 0.95], [0.2, 0.8]],
[[0.4, 0.2], [0.6, 0.1]],
[[0.9, 0.05], [0.3, 0.7]],
],
dtype=torch.float32,
)
okhsl_round_tripped = srgb_from_okhsl(okhsl_from_srgb(srgb))
okhsv_round_tripped = srgb_from_okhsv(okhsv_from_srgb(srgb))
assert torch.allclose(okhsl_round_tripped, srgb, atol=5e-4)
assert torch.allclose(okhsv_round_tripped, srgb, atol=5e-4)
def test_okhsl_and_okhsv_outputs_keep_hue_in_degrees_and_other_channels_in_unit_range() -> None:
srgb = torch.tensor(
[
[[0.0, 1.0]],
[[1.0, 0.0]],
[[0.5, 0.25]],
],
dtype=torch.float32,
)
okhsl = okhsl_from_srgb(srgb)
okhsv = okhsv_from_srgb(srgb)
assert torch.all(okhsl[0, ...] >= 0.0)
assert torch.all(okhsl[0, ...] <= 360.0)
assert torch.all(okhsl[1:, ...] >= 0.0)
assert torch.all(okhsl[1:, ...] <= 1.0)
assert torch.all(okhsv[0, ...] >= 0.0)
assert torch.all(okhsv[0, ...] <= 360.0)
assert torch.all(okhsv[1:, ...] >= 0.0)
assert torch.all(okhsv[1:, ...] <= 1.0)
def test_okhsl_and_okhsv_wrap_degree_hue_values() -> None:
okhsl = torch.tensor([[[389.2473]], [[1.0]], [[0.5681]]], dtype=torch.float32)
okhsl_wrapped = torch.tensor([[[29.2473]], [[1.0]], [[0.5681]]], dtype=torch.float32)
okhsv = torch.tensor([[[389.2473]], [[1.0]], [[1.0]]], dtype=torch.float32)
okhsv_wrapped = torch.tensor([[[29.2473]], [[1.0]], [[1.0]]], dtype=torch.float32)
assert torch.allclose(srgb_from_okhsl(okhsl), srgb_from_okhsl(okhsl_wrapped), atol=1e-5)
assert torch.allclose(srgb_from_okhsv(okhsv), srgb_from_okhsv(okhsv_wrapped), atol=1e-5)
def test_linear_srgb_xyz_round_trip() -> None:
linear_srgb = torch.tensor(
[
[[0.0, 1.0], [0.25, 0.8]],
[[0.2, 0.8], [0.75, 0.1]],
[[1.0, 0.1], [0.5, 0.4]],
],
dtype=torch.float32,
)
round_tripped = linear_srgb_from_xyz(xyz_from_linear_srgb(linear_srgb))
assert torch.allclose(round_tripped, linear_srgb, atol=5e-5)
def test_oklab_from_xyz_matches_explicit_conversion_path() -> None:
xyz = torch.tensor(
[
[[0.4124, 0.9505]],
[[0.2126, 1.0]],
[[0.0193, 1.0888]],
],
dtype=torch.float32,
)
direct = oklab_from_xyz(xyz)
via_linear_srgb = oklab_from_linear_srgb(linear_srgb_from_xyz(xyz))
assert torch.allclose(direct, via_linear_srgb, atol=1e-6)
def test_xyz_from_oklab_matches_explicit_conversion_path() -> None:
oklab = torch.tensor(
[
[[0.6, 0.4]],
[[0.2, -0.1]],
[[0.1, 0.05]],
],
dtype=torch.float32,
)
direct = xyz_from_oklab(oklab)
via_linear_srgb = xyz_from_linear_srgb(linear_srgb_from_oklab(oklab))
assert torch.allclose(direct, via_linear_srgb, atol=1e-6)
def test_oklch_from_xyz_matches_explicit_conversion_path() -> None:
xyz = torch.tensor(
[
[[0.4124, 0.9505]],
[[0.2126, 1.0]],
[[0.0193, 1.0888]],
],
dtype=torch.float32,
)
direct = oklch_from_xyz(xyz)
via_oklab = oklch_from_oklab(oklab_from_xyz(xyz))
assert torch.allclose(direct, via_oklab, atol=1e-6)
def test_xyz_from_oklch_matches_explicit_conversion_path() -> None:
oklch = torch.tensor(
[
[[0.7, 0.5]],
[[0.12, 0.04]],
[[30.0, 210.0]],
],
dtype=torch.float32,
)
direct = xyz_from_oklch(oklch)
via_oklab = xyz_from_oklab(oklab_from_oklch(oklch))
assert torch.allclose(direct, via_oklab, atol=1e-6)
def test_lab_from_linear_srgb_matches_explicit_conversion_path() -> None:
linear_srgb = torch.tensor(
[
[[0.0, 1.0], [0.25, 0.8]],
[[0.2, 0.8], [0.75, 0.1]],
[[1.0, 0.1], [0.5, 0.4]],
],
dtype=torch.float32,
)
direct = lab_from_linear_srgb(linear_srgb)
via_xyz = lab_from_xyz(xyz_from_linear_srgb(linear_srgb))
assert torch.allclose(direct, via_xyz, atol=1e-6)
def test_linear_srgb_from_lab_matches_explicit_conversion_path() -> None:
lab = torch.tensor(
[
[[0.0, 100.0], [50.0, 75.0]],
[[0.0, 0.0], [10.0, -20.0]],
[[0.0, 0.0], [-5.0, 30.0]],
],
dtype=torch.float32,
)
direct = linear_srgb_from_lab(lab)
via_xyz = linear_srgb_from_xyz(xyz_from_lab(lab))
assert torch.allclose(direct, via_xyz, atol=1e-6)
def test_srgb_xyz_round_trip() -> None:
srgb = torch.tensor(
[
[[0.0, 1.0], [0.25, 0.8]],
[[0.2, 0.8], [0.75, 0.1]],
[[1.0, 0.1], [0.5, 0.4]],
],
dtype=torch.float32,
)
round_tripped = srgb_from_xyz(xyz_from_srgb(srgb))
assert torch.allclose(round_tripped, srgb, atol=5e-4)
def test_lab_from_srgb_and_back_round_trip() -> None:
srgb = torch.tensor(
[
[[0.0, 1.0], [0.25, 0.8]],
[[0.2, 0.8], [0.75, 0.1]],
[[1.0, 0.1], [0.5, 0.4]],
],
dtype=torch.float32,
)
round_tripped = srgb_from_lab(lab_from_srgb(srgb))
assert torch.allclose(round_tripped, srgb, atol=5e-4)
def test_xyz_lab_round_trip_for_d65_and_d50() -> None:
xyz = torch.tensor(
[
[[0.4124, 0.9505]],
[[0.2126, 1.0]],
[[0.0193, 1.0888]],
],
dtype=torch.float32,
)
round_tripped_d65 = xyz_from_lab(lab_from_xyz(xyz, reference_illuminant="D65"), reference_illuminant="D65")
round_tripped_d50 = xyz_from_lab(lab_from_xyz(xyz, reference_illuminant="D50"), reference_illuminant="D50")
assert torch.allclose(round_tripped_d65, xyz, atol=1e-4)
assert torch.allclose(round_tripped_d50, xyz, atol=1e-4)
def test_xyz_d65_to_d50_maps_reference_white() -> None:
xyz_d65 = torch.tensor([[[0.950489]], [[1.0]], [[1.088840]]], dtype=torch.float32)
xyz_d50 = xyz_d65_to_d50(xyz_d65)
assert torch.allclose(xyz_d50[:, 0, 0], torch.tensor([0.964212, 1.0, 0.825188]), atol=5e-4)
def test_xyz_d50_to_d65_round_trip() -> None:
xyz_d65 = torch.tensor(
[
[[0.4124, 0.9505]],
[[0.2126, 1.0]],
[[0.0193, 1.0888]],
],
dtype=torch.float32,
)
round_tripped = xyz_d50_to_d65(xyz_d65_to_d50(xyz_d65))
assert torch.allclose(round_tripped, xyz_d65, atol=1e-5)
def test_lab_from_srgb_d50_matches_adapted_xyz_path() -> None:
srgb = torch.tensor(
[
[[0.0, 1.0], [0.25, 0.8]],
[[0.2, 0.8], [0.75, 0.1]],
[[1.0, 0.1], [0.5, 0.4]],
],
dtype=torch.float32,
)
direct = lab_from_srgb(srgb, reference_illuminant="D50")
via_xyz = lab_from_xyz(xyz_d65_to_d50(xyz_from_srgb(srgb)), reference_illuminant="D50")
assert torch.allclose(direct, via_xyz, atol=1e-4)
def test_lab_from_xyz_matches_reference_white_and_black() -> None:
xyz = torch.tensor(
[
[[0.0, 0.950489]],
[[0.0, 1.0]],
[[0.0, 1.088840]],
],
dtype=torch.float32,
)
lab = lab_from_xyz(xyz, reference_illuminant="D65")
assert torch.allclose(lab[:, 0, 0], torch.tensor([0.0, 0.0, 0.0]), atol=1e-4)
assert torch.allclose(lab[:, 0, 1], torch.tensor([100.0, 0.0, 0.0]), atol=1e-3)
def test_invalid_tensor_shape_raises_value_error() -> None:
with pytest.raises(ValueError, match="3xHxW"):
oklab_from_srgb(torch.zeros((2, 2), dtype=torch.float32))
def test_invalid_reference_illuminant_raises_value_error() -> None:
xyz = torch.ones((3, 1, 1), dtype=torch.float32)
with pytest.raises(ValueError, match="Unsupported reference_illuminant"):
lab_from_xyz(xyz, reference_illuminant="E")
def test_okhsl_from_srgb_forwards_steps_parameters(monkeypatch: pytest.MonkeyPatch) -> None:
recorded: dict[str, int] = {}
def fake_get_cs_tensor(
l_tensor: torch.Tensor, units_ab_tensor: torch.Tensor, steps: int = 1, steps_outer: int = 1
) -> torch.Tensor:
recorded["steps"] = steps
recorded["steps_outer"] = steps_outer
return torch.ones((3, *l_tensor.shape), dtype=l_tensor.dtype, device=l_tensor.device)
monkeypatch.setattr(color_conversion, "_get_cs_tensor", fake_get_cs_tensor)
srgb = torch.tensor(
[
[[0.2, 0.9]],
[[0.4, 0.3]],
[[0.8, 0.1]],
],
dtype=torch.float32,
)
color_conversion.okhsl_from_srgb(srgb, steps=3, steps_outer=4)
assert recorded == {"steps": 3, "steps_outer": 4}
def test_public_hsl_okhsl_okhsv_conversions_preserve_dtype_and_device() -> None:
srgb = torch.tensor(
[
[[0.05, 0.95], [0.2, 0.8]],
[[0.4, 0.2], [0.6, 0.1]],
[[0.9, 0.05], [0.3, 0.7]],
],
dtype=torch.float64,
)
hsl = hsl_from_srgb(srgb)
okhsl = okhsl_from_srgb(srgb)
okhsv = okhsv_from_srgb(srgb)
srgb_from_plain_hsl = srgb_from_hsl(hsl)
srgb_from_perceptual_hsl = srgb_from_okhsl(okhsl)
srgb_from_perceptual_hsv = srgb_from_okhsv(okhsv)
for output in (hsl, okhsl, okhsv, srgb_from_plain_hsl, srgb_from_perceptual_hsl, srgb_from_perceptual_hsv):
assert output.dtype == srgb.dtype
assert output.device == srgb.device
@@ -0,0 +1,106 @@
"""Tests for the mutable default argument fix in imwatermark/vendor.py
and the bare except fix in sqlite_database.py."""
from logging import Logger
from unittest import mock
import pytest
from invokeai.backend.image_util.imwatermark.vendor import EmbedMaxDct, WatermarkEncoder
class TestSetByBitsNoSharedState:
"""set_by_bits() used to have bits=[] as a default arg.
If it were still mutable, successive calls without an explicit arg
would accumulate state. After the fix (bits=None), each call gets
a fresh list."""
def test_set_by_bits_default_is_independent(self):
enc1 = WatermarkEncoder()
enc1.set_by_bits()
assert enc1._watermarks == []
assert enc1._wmLen == 0
enc2 = WatermarkEncoder()
enc2.set_by_bits()
assert enc2._watermarks == []
assert enc2._wmLen == 0
def test_set_by_bits_with_explicit_arg(self):
enc = WatermarkEncoder()
enc.set_by_bits([1, 0, 1])
assert enc._watermarks == [1, 0, 1]
assert enc._wmLen == 3
assert enc._wmType == "bits"
class TestEmbedMaxDctNoSharedState:
"""EmbedMaxDct.__init__ used to have watermarks=[] and scales=[0,36,36].
After the fix (both default to None), each instance gets its own list."""
def test_default_watermarks_independent(self):
e1 = EmbedMaxDct()
e1._watermarks.append(999)
e2 = EmbedMaxDct()
assert 999 not in e2._watermarks
assert e2._watermarks == []
def test_default_scales_independent(self):
e1 = EmbedMaxDct()
e1._scales.append(72)
e2 = EmbedMaxDct()
assert e2._scales == [0, 36, 36]
def test_explicit_args_still_work(self):
wm = [1, 0, 1, 1]
sc = [0, 50, 50]
e = EmbedMaxDct(watermarks=wm, wmLen=4, scales=sc, block=8)
assert e._watermarks == wm
assert e._wmLen == 4
assert e._scales == sc
assert e._block == 8
class TestTransactionExceptException:
"""The transaction() context manager used to have a bare `except:`.
After the fix it uses `except Exception:`, so BaseException subclasses
like KeyboardInterrupt and SystemExit should propagate instead of
being silently caught and rolled back."""
@staticmethod
def _make_db():
"""Create a minimal SqliteDatabase-like object with transaction()."""
# Import here so the test stays focused; we just need the real class.
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
logger = mock.MagicMock(spec=Logger)
db = SqliteDatabase(db_path=None, logger=logger, verbose=False)
return db
def test_regular_exception_rolls_back(self):
db = self._make_db()
# create a table first in a successful transaction
with db.transaction() as cursor:
cursor.execute("CREATE TABLE t (id INTEGER)")
# now try to insert and fail — the insert should be rolled back
with pytest.raises(ValueError):
with db.transaction() as cursor:
cursor.execute("INSERT INTO t VALUES (42)")
raise ValueError("boom")
# the row should not exist after rollback
with db.transaction() as cursor:
cursor.execute("SELECT * FROM t")
assert cursor.fetchone() is None
def test_keyboard_interrupt_propagates(self):
with pytest.raises(KeyboardInterrupt):
raise KeyboardInterrupt()
def test_system_exit_propagates(self):
with pytest.raises(SystemExit):
raise SystemExit(1)
@@ -0,0 +1,84 @@
import pytest
import torch
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
from invokeai.backend.stable_diffusion.diffusion.unet_attention_patcher import UNetAttentionPatcher
from invokeai.backend.util.test_utils import install_and_load_model
def build_dummy_sd15_unet_input(torch_device):
batch_size = 1
num_channels = 4
sizes = (32, 32)
noise = torch.randn((batch_size, num_channels) + sizes).to(torch_device)
time_step = torch.tensor([10]).to(torch_device)
encoder_hidden_states = torch.randn((batch_size, 77, 768)).to(torch_device)
return {"sample": noise, "timestep": time_step, "encoder_hidden_states": encoder_hidden_states}
@pytest.mark.parametrize(
"model_params",
[
# SD1.5, IPAdapter
{
"ip_adapter_model_id": "InvokeAI/ip_adapter_sd15",
"ip_adapter_model_name": "ip_adapter_sd15",
"base_model": BaseModelType.StableDiffusion1,
"unet_model_id": "runwayml/stable-diffusion-v1-5",
"unet_model_name": "stable-diffusion-v1-5",
},
# SD1.5, IPAdapterPlus
{
"ip_adapter_model_id": "InvokeAI/ip_adapter_plus_sd15",
"ip_adapter_model_name": "ip_adapter_plus_sd15",
"base_model": BaseModelType.StableDiffusion1,
"unet_model_id": "runwayml/stable-diffusion-v1-5",
"unet_model_name": "stable-diffusion-v1-5",
},
# SD1.5, IPAdapterFull
{
"ip_adapter_model_id": "InvokeAI/ip-adapter-full-face_sd15",
"ip_adapter_model_name": "ip-adapter-full-face_sd15",
"base_model": BaseModelType.StableDiffusion1,
"unet_model_id": "runwayml/stable-diffusion-v1-5",
"unet_model_name": "stable-diffusion-v1-5",
},
],
)
@pytest.mark.slow
def test_ip_adapter_unet_patch(model_params, model_installer, torch_device):
"""Smoke test that IP-Adapter weights can be loaded and used to patch a UNet."""
ip_adapter_info = install_and_load_model(
model_installer=model_installer,
model_path_id_or_url=model_params["ip_adapter_model_id"],
model_name=model_params["ip_adapter_model_name"],
base_model=model_params["base_model"],
model_type=ModelType.IPAdapter,
)
unet_info = install_and_load_model(
model_installer=model_installer,
model_path_id_or_url=model_params["unet_model_id"],
model_name=model_params["unet_model_name"],
base_model=model_params["base_model"],
model_type=ModelType.Main,
submodel_type=SubModelType.UNet,
)
dummy_unet_input = build_dummy_sd15_unet_input(torch_device)
with torch.no_grad(), ip_adapter_info as ip_adapter, unet_info as unet:
ip_adapter.to(torch_device, dtype=torch.float32)
unet.to(torch_device, dtype=torch.float32)
# ip_embeds shape: (batch_size, num_ip_images, seq_len, ip_image_embedding_len)
ip_embeds = torch.randn((1, 3, 4, 768)).to(torch_device)
cross_attention_kwargs = {"ip_adapter_image_prompt_embeds": [ip_embeds]}
ip_adapter_unet_patcher = UNetAttentionPatcher([ip_adapter])
with ip_adapter_unet_patcher.apply_ip_adapter_attention(unet):
output = unet(**dummy_unet_input, cross_attention_kwargs=cross_attention_kwargs).sample
assert output.shape == dummy_unet_input["sample"].shape
@@ -0,0 +1,193 @@
"""Tests for Anima ControlNet-LLLite config probing.
Anima LLLite adapters (v2 named-key format) are identified by the presence of both the shared
conditioning trunk (`lllite_conditioning1.*`) and per-module weights (`lllite_dit_blocks_*`).
SDXL ControlNet-LLLite models (`lllite_unet_*`) and Z-Image Control adapters
(`control_layers.*` etc.) must not match.
"""
import os
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from invokeai.backend.model_manager.configs.controlnet import (
ControlNet_Checkpoint_Anima_Config,
_has_anima_lllite_keys,
)
from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
# Opt-in test against the real adapter weights (anima-lllite-inpainting-v2.safetensors).
_REAL_WEIGHTS_ENV_VAR = "ANIMA_LLLITE_WEIGHTS_PATH"
REAL_WEIGHTS_PATH = Path(os.environ[_REAL_WEIGHTS_ENV_VAR]) if _REAL_WEIGHTS_ENV_VAR in os.environ else None
_OVERRIDE_FIELDS: dict[str, object] = {
"hash": "blake3:fakehash",
"path": "/fake/models/anima-lllite.safetensors",
"file_size": 1000,
"name": "anima-lllite",
"description": "test",
"source": "test",
"source_type": "path",
"key": "test-key",
}
ANIMA_LLLITE_KEYS = [
"lllite_conditioning1.conv1.weight",
"lllite_conditioning1.conv1.bias",
"lllite_conditioning1.proj.weight",
"lllite_conditioning1.out_norm.weight",
"lllite_dit_blocks_0_self_attn_q_proj.down.weight",
"lllite_dit_blocks_0_self_attn_q_proj.depth_embed",
"lllite_dit_blocks_27_mlp_layer1.up.weight",
]
SDXL_LLLITE_KEYS = [
"lllite_unet_input_blocks_4_1_transformer_blocks_0_attn1_to_q.cond_emb.weight",
"lllite_unet_input_blocks_4_1_transformer_blocks_0_attn1_to_q.down.0.weight",
"lllite_unet_middle_block_1_transformer_blocks_0_attn1_to_q.up.0.weight",
"lllite_unet_output_blocks_0_1_transformer_blocks_0_attn1_to_q.mid.0.weight",
]
Z_IMAGE_CONTROL_KEYS = [
"control_layers.0.attention.qkv.weight",
"control_layers.1.attention.out.weight",
"control_all_x_embedder.2-1.weight",
"control_noise_refiner.0.attention.qkv.weight",
]
def _make_state_dict(keys: list[str], conv1_in_channels: int = 3) -> dict[str, object]:
sd: dict[str, object] = dict.fromkeys(keys)
if "lllite_conditioning1.conv1.weight" in sd:
sd["lllite_conditioning1.conv1.weight"] = SimpleNamespace(shape=(160, conv1_in_channels, 4, 4))
return sd
def _make_mod(state_dict: dict[str, object], metadata: dict[str, str] | None = None) -> MagicMock:
mod = MagicMock()
mod.load_state_dict.return_value = state_dict
mod.metadata.return_value = metadata or {}
return mod
class TestHasAnimaLLLiteKeys:
"""Tests for the _has_anima_lllite_keys heuristic used during model identification."""
def test_anima_lllite_keys(self):
assert _has_anima_lllite_keys(_make_state_dict(ANIMA_LLLITE_KEYS)) is True
def test_trunk_only_does_not_match(self):
sd = _make_state_dict([k for k in ANIMA_LLLITE_KEYS if k.startswith("lllite_conditioning1.")])
assert _has_anima_lllite_keys(sd) is False
def test_modules_only_does_not_match(self):
sd = _make_state_dict([k for k in ANIMA_LLLITE_KEYS if k.startswith("lllite_dit_blocks_")])
assert _has_anima_lllite_keys(sd) is False
def test_sdxl_lllite_does_not_match(self):
assert _has_anima_lllite_keys(_make_state_dict(SDXL_LLLITE_KEYS)) is False
def test_z_image_control_does_not_match(self):
assert _has_anima_lllite_keys(_make_state_dict(Z_IMAGE_CONTROL_KEYS)) is False
def test_empty_state_dict(self):
assert _has_anima_lllite_keys({}) is False
class TestAnimaControlNetConfigProbe:
"""Tests for ControlNet_Checkpoint_Anima_Config.from_model_on_disk."""
def test_matches_anima_lllite(self):
mod = _make_mod(_make_state_dict(ANIMA_LLLITE_KEYS))
config = ControlNet_Checkpoint_Anima_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
assert config.base is BaseModelType.Anima
assert config.type is ModelType.ControlNet
assert config.format is ModelFormat.Checkpoint
@pytest.mark.parametrize(
"keys",
[SDXL_LLLITE_KEYS, Z_IMAGE_CONTROL_KEYS, []],
ids=["sdxl_lllite", "z_image_control", "empty"],
)
def test_rejects_non_anima_lllite(self, keys: list[str]):
mod = _make_mod(_make_state_dict(keys))
with pytest.raises(NotAMatchError, match="does not look like an Anima ControlNet-LLLite"):
ControlNet_Checkpoint_Anima_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
class TestCondInChannels:
"""Tests for the cond_in_channels field (metadata, conv1-shape fallback, old-JSON rehydration)."""
def test_populated_from_metadata(self):
mod = _make_mod(
_make_state_dict(ANIMA_LLLITE_KEYS, conv1_in_channels=3),
metadata={"lllite.cond_in_channels": "4"},
)
config = ControlNet_Checkpoint_Anima_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
assert config.cond_in_channels == 4
def test_fallback_to_conv1_shape_when_metadata_absent(self):
mod = _make_mod(_make_state_dict(ANIMA_LLLITE_KEYS, conv1_in_channels=4))
config = ControlNet_Checkpoint_Anima_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
assert config.cond_in_channels == 4
def test_old_json_rehydrates_with_none(self):
"""Configs stored before the field existed must still validate, with cond_in_channels=None."""
mod = _make_mod(_make_state_dict(ANIMA_LLLITE_KEYS))
config = ControlNet_Checkpoint_Anima_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
assert config.cond_in_channels == 3
stored = config.model_dump(mode="json")
del stored["cond_in_channels"]
rehydrated = ControlNet_Checkpoint_Anima_Config.model_validate(stored)
assert rehydrated.cond_in_channels is None
def test_override_skips_probe(self):
"""An explicit override must win without probing the state dict (which may be unprobeable)."""
keys = [k for k in ANIMA_LLLITE_KEYS if k != "lllite_conditioning1.conv1.weight"]
mod = _make_mod(_make_state_dict(keys))
config = ControlNet_Checkpoint_Anima_Config.from_model_on_disk(
mod, dict(_OVERRIDE_FIELDS) | {"cond_in_channels": 4}
)
assert config.cond_in_channels == 4
def test_missing_conv1_without_metadata_is_not_a_match(self):
"""A malformed file with LLLite keys but no conv1.weight must raise NotAMatchError, not KeyError."""
keys = [k for k in ANIMA_LLLITE_KEYS if k != "lllite_conditioning1.conv1.weight"]
mod = _make_mod(_make_state_dict(keys))
with pytest.raises(NotAMatchError, match="no lllite_conditioning1.conv1.weight"):
ControlNet_Checkpoint_Anima_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
@pytest.mark.skipif(
REAL_WEIGHTS_PATH is None,
reason=f"set {_REAL_WEIGHTS_ENV_VAR} to the real LLLite weights file to run",
)
def test_real_file_classifies_as_anima_controlnet():
"""The real adapter file must classify uniquely as an Anima ControlNet via the full factory."""
from invokeai.backend.model_manager.configs.factory import ModelConfigFactory
assert REAL_WEIGHTS_PATH is not None
# A stale path must fail loudly, not silently skip.
assert REAL_WEIGHTS_PATH.is_file(), f"{_REAL_WEIGHTS_ENV_VAR} points to a missing file: {REAL_WEIGHTS_PATH}"
result = ModelConfigFactory.from_model_on_disk(REAL_WEIGHTS_PATH, allow_unknown=False)
assert result.config is not None
assert isinstance(result.config, ControlNet_Checkpoint_Anima_Config)
assert result.match_count == 1
assert result.config.cond_in_channels == 4
@@ -0,0 +1,162 @@
import pytest
from invokeai.backend.model_manager.configs.main import _has_anima_keys
def _make_state_dict(prefixes: list[str], keys: list[str]) -> dict[str, object]:
"""Build a minimal fake state dict with the given prefixes applied to the given keys."""
return {f"{prefix}{key}": None for prefix in prefixes for key in keys}
# Minimal keys that satisfy both llm_adapter and cosmos DiT requirements
ANIMA_LLM_ADAPTER_KEYS = ["llm_adapter.blocks.0.cross_attn.k_norm.weight"]
ANIMA_COSMOS_DIT_KEYS = [
"blocks.0.adaln_modulation_cross_attn.1.weight",
"t_embedder.1.linear_1.weight",
"x_embedder.proj.1.weight",
"final_layer.adaln_modulation.1.weight",
]
class TestHasAnimaKeys:
"""Tests for _has_anima_keys heuristic used during model identification."""
def test_bare_keys(self):
"""Bare keys (no prefix) should be recognized."""
sd = _make_state_dict([""], ANIMA_LLM_ADAPTER_KEYS + ANIMA_COSMOS_DIT_KEYS)
assert _has_anima_keys(sd) is True
def test_net_prefix(self):
"""Official format with `net.` prefix should be recognized."""
sd = _make_state_dict(["net."], ANIMA_LLM_ADAPTER_KEYS + ANIMA_COSMOS_DIT_KEYS)
assert _has_anima_keys(sd) is True
def test_comfyui_bundled_prefix(self):
"""ComfyUI bundled format with `model.diffusion_model.` prefix should be recognized."""
sd = _make_state_dict(["model.diffusion_model."], ANIMA_LLM_ADAPTER_KEYS + ANIMA_COSMOS_DIT_KEYS)
assert _has_anima_keys(sd) is True
def test_comfyui_bundled_with_extra_keys(self):
"""Bundled checkpoint with VAE and text encoder keys should still be recognized."""
sd = _make_state_dict(["model.diffusion_model."], ANIMA_LLM_ADAPTER_KEYS + ANIMA_COSMOS_DIT_KEYS)
# Add bundled VAE and text encoder keys (should not interfere)
sd["first_stage_model.conv1.weight"] = None
sd["first_stage_model.encoder.downsamples.0.weight"] = None
sd["cond_stage_model.qwen3_06b.transformer.model.embed_tokens.weight"] = None
assert _has_anima_keys(sd) is True
def test_missing_llm_adapter_keys(self):
"""Should not match if llm_adapter keys are absent."""
sd = _make_state_dict([""], ANIMA_COSMOS_DIT_KEYS)
assert _has_anima_keys(sd) is False
def test_missing_cosmos_dit_keys(self):
"""Should not match if Cosmos DiT keys are absent."""
sd = _make_state_dict([""], ANIMA_LLM_ADAPTER_KEYS)
assert _has_anima_keys(sd) is False
def test_empty_state_dict(self):
"""Empty state dict should not match."""
assert _has_anima_keys({}) is False
def test_unrelated_keys(self):
"""State dict with unrelated keys should not match."""
sd = {
"model.diffusion_model.input_blocks.0.0.weight": None,
"model.diffusion_model.output_blocks.0.0.weight": None,
"cond_stage_model.transformer.text_model.embeddings.token_embedding.weight": None,
}
assert _has_anima_keys(sd) is False
@pytest.mark.parametrize(
"prefix",
["", "net.", "model.diffusion_model."],
)
def test_all_prefixes_parametrized(self, prefix: str):
"""All supported prefix formats should be recognized."""
sd = _make_state_dict([prefix], ANIMA_LLM_ADAPTER_KEYS + ANIMA_COSMOS_DIT_KEYS)
assert _has_anima_keys(sd) is True
class TestAnimaDoesNotConflictWithOtherModels:
"""Verify that _has_anima_keys does not false-positive on similar model architectures."""
def test_flux_bundled_checkpoint(self):
"""FLUX bundled checkpoints use double_blocks/single_blocks, not blocks — should not match."""
sd = {
"model.diffusion_model.double_blocks.0.img_attn.norm.key_norm.scale": None,
"model.diffusion_model.double_blocks.0.img_attn.proj.weight": None,
"model.diffusion_model.single_blocks.0.linear1.weight": None,
"model.diffusion_model.context_embedder.weight": None,
"model.diffusion_model.img_in.weight": None,
}
assert _has_anima_keys(sd) is False
def test_sd1_bundled_checkpoint(self):
"""SD1/SD2/SDXL bundled checkpoints use input_blocks/output_blocks — should not match."""
sd = {
"model.diffusion_model.input_blocks.0.0.weight": None,
"model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight": None,
"model.diffusion_model.output_blocks.0.0.weight": None,
"model.diffusion_model.middle_block.1.transformer_blocks.0.attn1.to_q.weight": None,
"first_stage_model.encoder.down.0.block.0.conv1.weight": None,
"cond_stage_model.transformer.text_model.embeddings.token_embedding.weight": None,
}
assert _has_anima_keys(sd) is False
def test_raw_cosmos_dit_without_llm_adapter(self):
"""A raw Cosmos Predict2 DiT (without Anima's LLM adapter) should not match."""
sd = {
"blocks.0.adaln_modulation_cross_attn.1.weight": None,
"blocks.0.self_attn.q_proj.weight": None,
"t_embedder.1.linear_1.weight": None,
"x_embedder.proj.1.weight": None,
"final_layer.adaln_modulation.1.weight": None,
}
assert _has_anima_keys(sd) is False
def test_z_image_checkpoint(self):
"""Z-Image uses blocks.* but with cap_embedder/context_refiner — should not match."""
sd = {
"model.diffusion_model.blocks.0.attn.to_q.weight": None,
"model.diffusion_model.blocks.0.attn.to_k.weight": None,
"model.diffusion_model.cap_embedder.0.weight": None,
"model.diffusion_model.context_refiner.blocks.0.weight": None,
"model.diffusion_model.t_embedder.mlp.0.weight": None,
"model.diffusion_model.x_embedder.proj.weight": None,
}
# Z-Image has blocks/t_embedder/x_embedder but NOT llm_adapter
assert _has_anima_keys(sd) is False
def test_qwen_image_checkpoint(self):
"""QwenImage uses txt_in/txt_norm/img_in — should not match."""
sd = {
"txt_in.weight": None,
"txt_norm.weight": None,
"img_in.weight": None,
"double_blocks.0.img_attn.proj.weight": None,
"single_blocks.0.linear1.weight": None,
}
assert _has_anima_keys(sd) is False
def test_flux_lora_does_not_match(self):
"""FLUX LoRA weights should not match as Anima."""
sd = {
"double_blocks.0.img_attn.proj.lora_down.weight": None,
"double_blocks.0.img_attn.proj.lora_up.weight": None,
"single_blocks.0.linear1.lora_down.weight": None,
}
assert _has_anima_keys(sd) is False
def test_cosmos_dit_bundled_without_llm_adapter(self):
"""Bundled Cosmos DiT (model.diffusion_model. prefix) but no llm_adapter — should not match."""
sd = {
"model.diffusion_model.blocks.0.self_attn.q_proj.weight": None,
"model.diffusion_model.t_embedder.1.linear_1.weight": None,
"model.diffusion_model.x_embedder.proj.1.weight": None,
"model.diffusion_model.final_layer.adaln_modulation.1.weight": None,
"first_stage_model.encoder.downsamples.0.weight": None,
"cond_stage_model.transformer.model.embed_tokens.weight": None,
}
# Has all the Cosmos DiT keys but missing llm_adapter — not Anima
assert _has_anima_keys(sd) is False
@@ -0,0 +1,115 @@
"""Regression tests for the double-variant kwarg bug.
When override_fields contains a field (variant, repo_variant, prediction_type, etc.)
that is also computed and passed as an explicit kwarg to cls(), using .get() instead
of .pop() causes TypeError("got multiple values for keyword argument ...").
These tests verify that .pop() is used consistently, so override values don't conflict
with explicitly computed values.
"""
from pathlib import Path
from unittest.mock import MagicMock, patch
from invokeai.backend.model_manager.taxonomy import QwenImageVariantType
# Required fields for the Pydantic config model
_REQUIRED_FIELDS = {
"hash": "blake3:fakehash",
"path": "/fake/models/test-model",
"file_size": 1000,
"name": "test-model",
"description": "test",
"source": "test",
"source_type": "path",
"key": "test-key",
}
def _make_mock_dir(dirname: str = "test-model") -> MagicMock:
"""Create a mock ModelOnDisk for a Diffusers directory."""
mod = MagicMock()
mod.path = Path(f"/fake/models/{dirname}")
return mod
class TestDoubleVariantRegression:
"""Verify that override_fields with variant/repo_variant don't cause double-kwarg errors."""
@patch("invokeai.backend.model_manager.configs.main.raise_for_class_name")
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_dir")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_qwen_image_diffusers_with_variant_in_overrides(self, _rfo, _rid, _rfc):
"""Installing a Qwen Image Edit Diffusers model with variant in override_fields should not crash."""
from invokeai.backend.model_manager.configs.main import Main_Diffusers_QwenImage_Config
mod = _make_mock_dir("Qwen-Image-Edit-2511")
# Simulate what happens when a starter model provides variant
overrides = {
**_REQUIRED_FIELDS,
"variant": QwenImageVariantType.Edit,
}
from invokeai.backend.model_manager.configs.base import ModelRepoVariant
with patch.object(
Main_Diffusers_QwenImage_Config, "_get_repo_variant_or_raise", return_value=ModelRepoVariant("")
):
with patch.object(
Main_Diffusers_QwenImage_Config,
"_get_qwen_image_variant",
return_value=QwenImageVariantType.Edit,
):
# This would previously raise: TypeError("got multiple values for keyword argument 'variant'")
config = Main_Diffusers_QwenImage_Config.from_model_on_disk(mod, overrides)
assert config.variant == QwenImageVariantType.Edit
@patch("invokeai.backend.model_manager.configs.main.raise_for_class_name")
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_dir")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_qwen_image_diffusers_override_variant_takes_precedence(self, _rfo, _rid, _rfc):
"""An explicit variant override should take precedence over auto-detection."""
from invokeai.backend.model_manager.configs.base import ModelRepoVariant
from invokeai.backend.model_manager.configs.main import Main_Diffusers_QwenImage_Config
mod = _make_mock_dir("Qwen-Image-2512")
overrides = {
**_REQUIRED_FIELDS,
"variant": QwenImageVariantType.Edit, # explicitly override to Edit
}
with patch.object(
Main_Diffusers_QwenImage_Config, "_get_repo_variant_or_raise", return_value=ModelRepoVariant("")
):
with patch.object(
Main_Diffusers_QwenImage_Config,
"_get_qwen_image_variant",
return_value=QwenImageVariantType.Generate, # auto-detect says Generate
):
config = Main_Diffusers_QwenImage_Config.from_model_on_disk(mod, overrides)
# Override should win over auto-detection
assert config.variant == QwenImageVariantType.Edit
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=True)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_qwen_image_gguf_with_variant_in_overrides(self, _rfo, _rif, _hgt, _hqk):
"""Installing a Qwen Image Edit GGUF with variant in override_fields should not crash."""
from invokeai.backend.model_manager.configs.main import Main_GGUF_QwenImage_Config
mod = MagicMock()
mod.path = Path("/fake/models/qwen-image-edit-2511-Q4_K_M.gguf")
mod.load_state_dict.return_value = {}
overrides = {
**_REQUIRED_FIELDS,
"variant": QwenImageVariantType.Edit,
}
config = Main_GGUF_QwenImage_Config.from_model_on_disk(mod, overrides)
assert config.variant == QwenImageVariantType.Edit
@@ -0,0 +1,112 @@
import pytest
from pydantic import ValidationError
from invokeai.backend.model_manager.configs.lora import LoraModelDefaultSettings
def test_accepts_none_for_all_fields() -> None:
settings = LoraModelDefaultSettings()
assert settings.weight is None
assert settings.weight_min is None
assert settings.weight_max is None
def test_accepts_both_bounds_with_min_less_than_max() -> None:
settings = LoraModelDefaultSettings(weight_min=-2.0, weight_max=3.0)
assert settings.weight_min == -2.0
assert settings.weight_max == 3.0
def test_rejects_both_bounds_with_min_greater_than_or_equal_to_max() -> None:
with pytest.raises(ValidationError):
LoraModelDefaultSettings(weight_min=2.0, weight_max=2.0)
with pytest.raises(ValidationError):
LoraModelDefaultSettings(weight_min=3.0, weight_max=1.0)
def test_accepts_only_weight_min_within_default_range() -> None:
# Default max is 2.0; a weight_min of 1.0 leaves a valid effective range [1.0, 2.0].
settings = LoraModelDefaultSettings(weight_min=1.0)
assert settings.weight_min == 1.0
assert settings.weight_max is None
def test_rejects_only_weight_min_above_default_max() -> None:
# Reproduces the partial-bound bug: saving only weight_min=3 used to be accepted but
# rendered as a min>max slider in LoRACard. The effective max defaults to 2.0.
with pytest.raises(ValidationError):
LoraModelDefaultSettings(weight_min=3.0)
def test_rejects_only_weight_min_equal_to_default_max() -> None:
with pytest.raises(ValidationError):
LoraModelDefaultSettings(weight_min=2.0)
def test_accepts_only_weight_max_within_default_range() -> None:
# Default min is -1.0; a weight_max of 0.5 leaves a valid effective range [-1.0, 0.5].
settings = LoraModelDefaultSettings(weight_max=0.5)
assert settings.weight_max == 0.5
assert settings.weight_min is None
def test_rejects_only_weight_max_below_default_min() -> None:
# Reproduces the symmetric partial-bound bug for weight_max <= -1.
with pytest.raises(ValidationError):
LoraModelDefaultSettings(weight_max=-2.0)
def test_rejects_only_weight_max_equal_to_default_min() -> None:
with pytest.raises(ValidationError):
LoraModelDefaultSettings(weight_max=-1.0)
def test_accepts_weight_within_effective_range_with_explicit_bounds() -> None:
settings = LoraModelDefaultSettings(weight=0.5, weight_min=-1.0, weight_max=2.0)
assert settings.weight == 0.5
def test_accepts_weight_within_default_effective_range() -> None:
# With no bounds set the effective range is [-1.0, 2.0].
settings = LoraModelDefaultSettings(weight=1.5)
assert settings.weight == 1.5
def test_rejects_weight_above_explicit_effective_range() -> None:
# Reproduces the out-of-range-weight bug: weight=10 with bounds [-1, 2] used to be
# accepted but the slider in LoRACard could only show [-1, 2].
with pytest.raises(ValidationError):
LoraModelDefaultSettings(weight=10.0, weight_min=-1.0, weight_max=2.0)
def test_rejects_weight_below_explicit_effective_range() -> None:
with pytest.raises(ValidationError):
LoraModelDefaultSettings(weight=-5.0, weight_min=-1.0, weight_max=2.0)
def test_rejects_weight_above_default_effective_range() -> None:
# No bounds set; default max is 2.0.
with pytest.raises(ValidationError):
LoraModelDefaultSettings(weight=2.5)
def test_rejects_weight_outside_partial_bound() -> None:
# Only weight_min set; weight must respect effective range [weight_min, default_max].
with pytest.raises(ValidationError):
LoraModelDefaultSettings(weight=0.0, weight_min=0.5)
# Only weight_max set; weight must respect effective range [default_min, weight_max].
with pytest.raises(ValidationError):
LoraModelDefaultSettings(weight=1.0, weight_max=0.5)
def test_accepts_weight_at_effective_bounds() -> None:
# Inclusive bounds.
LoraModelDefaultSettings(weight=-1.0, weight_min=-1.0, weight_max=2.0)
LoraModelDefaultSettings(weight=2.0, weight_min=-1.0, weight_max=2.0)
def test_rejects_extra_fields() -> None:
with pytest.raises(ValidationError):
LoraModelDefaultSettings(extra_field=True) # type: ignore[call-arg]
@@ -0,0 +1,81 @@
"""Regression tests for Qwen3 Encoder config probing.
See https://github.com/invoke-ai/InvokeAI/issues/9090
`Qwen2.5-1.5B-Instruct` (a standalone causal LM) was being misidentified as a
`Qwen3Encoder` because the diffusers-style config check matched any directory with
`config.json` at the root and a Qwen* class name. A complete causal LM also bundles
tokenizer files at the root, while standalone text_encoder downloads do not — we
use that to disambiguate.
"""
import json
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import MagicMock
import pytest
from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
from invokeai.backend.model_manager.configs.qwen3_encoder import Qwen3Encoder_Qwen3Encoder_Config
_OVERRIDE_FIELDS: dict[str, object] = {
"hash": "blake3:fakehash",
"path": "/fake/models/test-model",
"file_size": 1000,
"name": "test-model",
"description": "test",
"source": "test",
"source_type": "path",
"key": "test-key",
}
def _write_config(path: Path, hidden_size: int = 2560, architecture: str = "Qwen2ForCausalLM") -> None:
path.write_text(json.dumps({"architectures": [architecture], "hidden_size": hidden_size}))
@pytest.mark.parametrize("tokenizer_file", ["tokenizer.json", "tokenizer.model", "tokenizer_config.json"])
def test_complete_causal_lm_is_rejected(tokenizer_file: str) -> None:
"""A directory with config.json + tokenizer files at root is a TextLLM, not a Qwen3 encoder."""
with TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
_write_config(root / "config.json")
(root / tokenizer_file).write_text("{}")
mod = MagicMock()
mod.path = root
with pytest.raises(NotAMatchError, match="complete causal LM"):
Qwen3Encoder_Qwen3Encoder_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
def test_standalone_text_encoder_subfolder_still_matches() -> None:
"""A standalone text_encoder download (config.json at root, no tokenizer files) should still match."""
with TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
_write_config(root / "config.json")
mod = MagicMock()
mod.path = root
config = Qwen3Encoder_Qwen3Encoder_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
assert config.type.value == "qwen3_encoder"
def test_nested_text_encoder_with_root_tokenizer_still_matches() -> None:
"""A model with text_encoder/config.json should match even if tokenizer files exist at root.
The tokenizer-at-root heuristic only applies to the standalone (root-level config.json) case.
"""
with TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "text_encoder").mkdir()
_write_config(root / "text_encoder" / "config.json")
(root / "tokenizer.json").write_text("{}")
mod = MagicMock()
mod.path = root
config = Qwen3Encoder_Qwen3Encoder_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
assert config.type.value == "qwen3_encoder"
@@ -0,0 +1,187 @@
"""Tests for Qwen Image single-file checkpoint variant detection.
Mirrors `test_qwen_image_gguf_variant_detection.py`. The Checkpoint and GGUF
configs share the same variant inference (`_infer_qwen_image_variant`):
1. Explicit `variant` in override_fields wins.
2. Presence of the `__index_timestep_zero__` tensor → Edit.
3. Filename heuristic: "edit" substring in the stem → Edit.
4. Otherwise default to Generate.
Also tests that the Checkpoint config rejects GGUF state dicts (and vice
versa), so the two configs don't both match the same file.
"""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
from invokeai.backend.model_manager.taxonomy import QwenImageVariantType
_REQUIRED_FIELDS = {
"hash": "blake3:fakehash",
"path": "/fake/models/test.safetensors",
"file_size": 1000,
"name": "test-model",
"description": "test",
"source": "test",
"source_type": "path",
"key": "test-key",
}
class TestCheckpointQwenImageVariantDetection:
def _make_mock_mod(self, filename: str) -> MagicMock:
mod = MagicMock()
mod.path = Path(f"/fake/models/{filename}")
return mod
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=False)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_edit_in_filename_sets_edit_variant(self, _rfo, _rif, _hgt, _hqk):
from invokeai.backend.model_manager.configs.main import Main_Checkpoint_QwenImage_Config
mod = self._make_mock_mod("qwen-image-edit-2511-fp8.safetensors")
mod.load_state_dict.return_value = {}
config = Main_Checkpoint_QwenImage_Config.from_model_on_disk(mod, {**_REQUIRED_FIELDS})
assert config.variant == QwenImageVariantType.Edit
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=False)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_edit_case_insensitive(self, _rfo, _rif, _hgt, _hqk):
from invokeai.backend.model_manager.configs.main import Main_Checkpoint_QwenImage_Config
mod = self._make_mock_mod("Qwen-Image-EDIT-2511-fp8.safetensors")
mod.load_state_dict.return_value = {}
config = Main_Checkpoint_QwenImage_Config.from_model_on_disk(mod, {**_REQUIRED_FIELDS})
assert config.variant == QwenImageVariantType.Edit
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=False)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_no_marker_no_edit_in_filename_defaults_to_generate(self, _rfo, _rif, _hgt, _hqk):
from invokeai.backend.model_manager.configs.main import Main_Checkpoint_QwenImage_Config
mod = self._make_mock_mod("qwen-image-2512-bf16.safetensors")
mod.load_state_dict.return_value = {}
config = Main_Checkpoint_QwenImage_Config.from_model_on_disk(mod, {**_REQUIRED_FIELDS})
assert config.variant == QwenImageVariantType.Generate
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=False)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_marker_tensor_sets_edit_variant(self, _rfo, _rif, _hgt, _hqk):
from invokeai.backend.model_manager.configs.main import Main_Checkpoint_QwenImage_Config
mod = self._make_mock_mod("some-arbitrary-name.safetensors")
mod.load_state_dict.return_value = {"__index_timestep_zero__": object()}
config = Main_Checkpoint_QwenImage_Config.from_model_on_disk(mod, {**_REQUIRED_FIELDS})
assert config.variant == QwenImageVariantType.Edit
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=False)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_explicit_variant_override_not_overwritten(self, _rfo, _rif, _hgt, _hqk):
from invokeai.backend.model_manager.configs.main import Main_Checkpoint_QwenImage_Config
mod = self._make_mock_mod("qwen-image-edit-2511-fp8.safetensors")
mod.load_state_dict.return_value = {}
config = Main_Checkpoint_QwenImage_Config.from_model_on_disk(
mod, {**_REQUIRED_FIELDS, "variant": QwenImageVariantType.Generate}
)
assert config.variant == QwenImageVariantType.Generate
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=True)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_rejects_gguf_state_dict(self, _rfo, _rif, _hgt, _hqk):
"""Checkpoint config must NOT match files that look GGUF-quantized."""
from invokeai.backend.model_manager.configs.main import Main_Checkpoint_QwenImage_Config
mod = self._make_mock_mod("qwen-image-edit-2511-Q4_K_M.gguf")
mod.load_state_dict.return_value = {}
with pytest.raises(NotAMatchError):
Main_Checkpoint_QwenImage_Config.from_model_on_disk(mod, {**_REQUIRED_FIELDS})
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=False)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=False)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_rejects_non_qwen_state_dict(self, _rfo, _rif, _hgt, _hqk):
"""Checkpoint config must NOT match files whose state dict isn't Qwen Image."""
from invokeai.backend.model_manager.configs.main import Main_Checkpoint_QwenImage_Config
mod = self._make_mock_mod("not-a-qwen-model.safetensors")
mod.load_state_dict.return_value = {}
with pytest.raises(NotAMatchError):
Main_Checkpoint_QwenImage_Config.from_model_on_disk(mod, {**_REQUIRED_FIELDS})
class TestHasQwenImageKeys:
"""Detection must agree with the loader, which strips ComfyUI prefixes before loading."""
def test_bare_keys_detected(self):
from invokeai.backend.model_manager.configs.main import _has_qwen_image_keys
sd = {"txt_in.weight": 1, "txt_norm.weight": 1, "img_in.weight": 1}
assert _has_qwen_image_keys(sd)
@pytest.mark.parametrize("prefix", ["model.diffusion_model.", "diffusion_model."])
def test_comfyui_prefixed_keys_detected(self, prefix: str):
"""A ComfyUI checkpoint with prefixed keys must still be identified so it reaches the loader."""
from invokeai.backend.model_manager.configs.main import _has_qwen_image_keys
sd = {f"{prefix}txt_in.weight": 1, f"{prefix}txt_norm.weight": 1, f"{prefix}img_in.weight": 1}
assert _has_qwen_image_keys(sd)
def test_flux_rejected(self):
from invokeai.backend.model_manager.configs.main import _has_qwen_image_keys
sd = {"txt_in.weight": 1, "txt_norm.weight": 1, "img_in.weight": 1, "context_embedder.weight": 1}
assert not _has_qwen_image_keys(sd)
def test_prefixed_marker_sets_edit_variant(self):
"""The Edit marker tensor may also carry a ComfyUI prefix."""
from invokeai.backend.model_manager.configs.main import _infer_qwen_image_variant
from invokeai.backend.model_manager.taxonomy import QwenImageVariantType
sd = {"model.diffusion_model.__index_timestep_zero__": object()}
assert _infer_qwen_image_variant(sd, Path("/fake/plain-name.safetensors")) == QwenImageVariantType.Edit
class TestEditTokenHeuristic:
"""The filename "edit" heuristic must match the token, not any substring."""
@pytest.mark.parametrize(
"stem",
["qwen-image-edit-2511", "qwen_image_edit_2509", "Qwen-Image-EDIT", "model.edit"],
)
def test_edit_token_matches(self, stem: str):
from invokeai.backend.model_manager.configs.main import _infer_qwen_image_variant
from invokeai.backend.model_manager.taxonomy import QwenImageVariantType
assert _infer_qwen_image_variant({}, Path(f"/fake/{stem}.safetensors")) == QwenImageVariantType.Edit
@pytest.mark.parametrize("stem", ["credited-model", "edited-final", "unedited", "qwen-image"])
def test_edit_substring_does_not_false_positive(self, stem: str):
from invokeai.backend.model_manager.configs.main import _infer_qwen_image_variant
from invokeai.backend.model_manager.taxonomy import QwenImageVariantType
assert _infer_qwen_image_variant({}, Path(f"/fake/{stem}.safetensors")) == QwenImageVariantType.Generate
@@ -0,0 +1,122 @@
"""Tests for GGUF Qwen Image variant detection.
Detection precedence:
1. Explicit `variant` in override_fields wins.
2. Presence of the `__index_timestep_zero__` tensor in the state dict marks an Edit model.
3. Otherwise fall back to a filename heuristic ("edit" in the stem → Edit).
4. Otherwise default to Generate.
"""
from pathlib import Path
from unittest.mock import MagicMock, patch
from invokeai.backend.model_manager.taxonomy import QwenImageVariantType
# Required fields for the Pydantic config model
_REQUIRED_FIELDS = {
"hash": "blake3:fakehash",
"path": "/fake/models/test.gguf",
"file_size": 1000,
"name": "test-model",
"description": "test",
"source": "test",
"source_type": "path",
"key": "test-key",
}
class TestGGUFQwenImageVariantDetection:
"""Test that GGUF Qwen Image models infer the edit variant from filename."""
def _make_mock_mod(self, filename: str) -> MagicMock:
"""Create a mock ModelOnDisk with the given filename."""
mod = MagicMock()
mod.path = Path(f"/fake/models/{filename}")
return mod
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=True)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_edit_in_filename_sets_edit_variant(self, _rfo, _rif, _hgt, _hqk):
"""A GGUF file with 'edit' in the name should be tagged as edit variant."""
from invokeai.backend.model_manager.configs.main import Main_GGUF_QwenImage_Config
mod = self._make_mock_mod("qwen-image-edit-2511-Q4_K_M.gguf")
mod.load_state_dict.return_value = {}
config = Main_GGUF_QwenImage_Config.from_model_on_disk(mod, {**_REQUIRED_FIELDS})
assert config.variant == QwenImageVariantType.Edit
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=True)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_edit_case_insensitive(self, _rfo, _rif, _hgt, _hqk):
"""The 'edit' check should be case-insensitive."""
from invokeai.backend.model_manager.configs.main import Main_GGUF_QwenImage_Config
mod = self._make_mock_mod("Qwen-Image-EDIT-2511-Q8_0.gguf")
mod.load_state_dict.return_value = {}
config = Main_GGUF_QwenImage_Config.from_model_on_disk(mod, {**_REQUIRED_FIELDS})
assert config.variant == QwenImageVariantType.Edit
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=True)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_no_marker_no_edit_in_filename_defaults_to_generate(self, _rfo, _rif, _hgt, _hqk):
"""A GGUF file without the marker tensor or 'edit' in the name should default to Generate."""
from invokeai.backend.model_manager.configs.main import Main_GGUF_QwenImage_Config
mod = self._make_mock_mod("qwen-image-2512-Q4_K_M.gguf")
mod.load_state_dict.return_value = {}
config = Main_GGUF_QwenImage_Config.from_model_on_disk(mod, {**_REQUIRED_FIELDS})
assert config.variant == QwenImageVariantType.Generate
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=True)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_marker_tensor_sets_edit_variant(self, _rfo, _rif, _hgt, _hqk):
"""Presence of `__index_timestep_zero__` in the state dict should set the Edit variant."""
from invokeai.backend.model_manager.configs.main import Main_GGUF_QwenImage_Config
# Filename has no "edit" marker, but the tensor is present
mod = self._make_mock_mod("some-arbitrary-name.gguf")
mod.load_state_dict.return_value = {"__index_timestep_zero__": object()}
config = Main_GGUF_QwenImage_Config.from_model_on_disk(mod, {**_REQUIRED_FIELDS})
assert config.variant == QwenImageVariantType.Edit
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=True)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_marker_tensor_takes_precedence_over_filename(self, _rfo, _rif, _hgt, _hqk):
"""The marker tensor wins even when the filename has no 'edit' substring."""
from invokeai.backend.model_manager.configs.main import Main_GGUF_QwenImage_Config
mod = self._make_mock_mod("qwen-image-2512-Q4_K_M.gguf")
mod.load_state_dict.return_value = {"__index_timestep_zero__": object()}
config = Main_GGUF_QwenImage_Config.from_model_on_disk(mod, {**_REQUIRED_FIELDS})
assert config.variant == QwenImageVariantType.Edit
@patch("invokeai.backend.model_manager.configs.main._has_qwen_image_keys", return_value=True)
@patch("invokeai.backend.model_manager.configs.main._has_ggml_tensors", return_value=True)
@patch("invokeai.backend.model_manager.configs.main.raise_if_not_file")
@patch("invokeai.backend.model_manager.configs.main.raise_for_override_fields")
def test_explicit_variant_override_not_overwritten(self, _rfo, _rif, _hgt, _hqk):
"""An explicit variant in override_fields should not be overwritten by filename heuristic."""
from invokeai.backend.model_manager.configs.main import Main_GGUF_QwenImage_Config
mod = self._make_mock_mod("qwen-image-edit-2511-Q4_K_M.gguf")
mod.load_state_dict.return_value = {}
config = Main_GGUF_QwenImage_Config.from_model_on_disk(
mod, {**_REQUIRED_FIELDS, "variant": QwenImageVariantType.Generate}
)
assert config.variant == QwenImageVariantType.Generate
@@ -0,0 +1,52 @@
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import MagicMock
import gguf
import pytest
import torch
from invokeai.backend.model_manager.configs.main import Main_GGUF_QwenImage_Config
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
def _build_ggml_tensor() -> GGMLTensor:
return GGMLTensor(
data=torch.zeros((1,), dtype=torch.uint8),
ggml_quantization_type=gguf.GGMLQuantizationType.Q4_0,
tensor_shape=torch.Size([1, 1]),
compute_dtype=torch.float32,
)
@pytest.mark.parametrize("is_edit_model", [True, False])
def test_qwen_gguf_config_sets_a_variant_for_imported_models(is_edit_model: bool) -> None:
with TemporaryDirectory() as tmpdir:
model_path = Path(tmpdir) / ("qwen-image-edit.gguf" if is_edit_model else "qwen-image.gguf")
model_name = "Qwen Image Edit GGUF" if is_edit_model else "Qwen Image GGUF"
model_path.touch()
mod = MagicMock()
mod.path = model_path
mod.load_state_dict.return_value = {
"txt_in.weight": _build_ggml_tensor(),
"txt_norm.weight": _build_ggml_tensor(),
"img_in.weight": _build_ggml_tensor(),
}
config = Main_GGUF_QwenImage_Config.from_model_on_disk(
mod,
{
"hash": "test-hash",
"path": str(model_path),
"file_size": model_path.stat().st_size,
"name": model_name,
"source": str(model_path),
"source_type": "path",
},
)
if is_edit_model:
assert config.variant == "edit"
else:
assert config.variant == "generate"
@@ -0,0 +1,101 @@
"""Tests for Qwen VL encoder config identification.
The single-file checkpoint identifier reads only the safetensors key index
instead of loading the full tensor data — a 7GB fp8 encoder otherwise pins
~7GB of RAM during model scan.
"""
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import MagicMock
import pytest
import torch
from safetensors.torch import save_file
from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
from invokeai.backend.model_manager.configs.qwen_vl_encoder import (
QwenVLEncoder_Checkpoint_Config,
_has_qwen_vl_keys,
_read_safetensors_keys,
)
_OVERRIDE_FIELDS: dict[str, object] = {
"hash": "blake3:fakehash",
"path": "/fake/models/test-model.safetensors",
"file_size": 1000,
"name": "test-model",
"description": "test",
"source": "test",
"source_type": "path",
"key": "test-key",
}
def _write_safetensors(path: Path, keys: list[str]) -> None:
"""Write a safetensors file with tiny placeholder tensors for the given keys."""
sd = {k: torch.zeros(1, dtype=torch.float32) for k in keys}
save_file(sd, str(path))
def test_has_qwen_vl_keys_accepts_lm_plus_visual() -> None:
assert _has_qwen_vl_keys(["model.embed_tokens.weight", "visual.patch_embed.proj.weight"])
assert _has_qwen_vl_keys(["model.layers.0.self_attn.q_proj.weight", "visual.blocks.0.norm1.weight"])
def test_has_qwen_vl_keys_rejects_lm_only() -> None:
"""Text-only Qwen3/Qwen2 encoders have LM keys but no visual tower — must not match."""
assert not _has_qwen_vl_keys(["model.embed_tokens.weight", "model.layers.0.self_attn.q_proj.weight"])
def test_has_qwen_vl_keys_rejects_empty() -> None:
assert not _has_qwen_vl_keys([])
def test_read_safetensors_keys_returns_keys_without_loading_tensors() -> None:
with TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "tiny.safetensors"
_write_safetensors(path, ["model.embed_tokens.weight", "visual.patch_embed.proj.weight"])
keys = _read_safetensors_keys(path)
assert set(keys) == {"model.embed_tokens.weight", "visual.patch_embed.proj.weight"}
def test_checkpoint_config_matches_qwen_vl_safetensors() -> None:
with TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "qwen_vl.safetensors"
_write_safetensors(path, ["model.embed_tokens.weight", "visual.patch_embed.proj.weight"])
mod = MagicMock()
mod.path = path
config = QwenVLEncoder_Checkpoint_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
assert config.type.value == "qwen_vl_encoder"
assert config.format.value == "checkpoint"
def test_checkpoint_config_rejects_lm_only_safetensors() -> None:
"""A text-only LM checkpoint must not be identified as a Qwen VL encoder."""
with TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "text_only.safetensors"
_write_safetensors(path, ["model.embed_tokens.weight", "model.layers.0.self_attn.q_proj.weight"])
mod = MagicMock()
mod.path = path
with pytest.raises(NotAMatchError, match="does not look like a Qwen2.5-VL"):
QwenVLEncoder_Checkpoint_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
def test_checkpoint_config_rejects_non_safetensors_extension() -> None:
"""Bin/ckpt/pt files should be rejected cheaply without attempting to read the header."""
with TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "weights.bin"
path.write_bytes(b"not a safetensors file")
mod = MagicMock()
mod.path = path
with pytest.raises(NotAMatchError, match="expected a .safetensors file"):
QwenVLEncoder_Checkpoint_Config.from_model_on_disk(mod, dict(_OVERRIDE_FIELDS))
@@ -0,0 +1 @@
This is an empty invokeai root that is used as a template for model manager tests.
@@ -0,0 +1,79 @@
model:
base_learning_rate: 1.0e-04
target: invokeai.backend.models.diffusion.ddpm.LatentDiffusion
params:
linear_start: 0.00085
linear_end: 0.0120
num_timesteps_cond: 1
log_every_t: 200
timesteps: 1000
first_stage_key: "jpg"
cond_stage_key: "txt"
image_size: 64
channels: 4
cond_stage_trainable: false # Note: different from the one we trained before
conditioning_key: crossattn
monitor: val/loss_simple_ema
scale_factor: 0.18215
use_ema: False
scheduler_config: # 10000 warmup steps
target: invokeai.backend.stable_diffusion.lr_scheduler.LambdaLinearScheduler
params:
warm_up_steps: [ 10000 ]
cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases
f_start: [ 1.e-6 ]
f_max: [ 1. ]
f_min: [ 1. ]
personalization_config:
target: invokeai.backend.stable_diffusion.embedding_manager.EmbeddingManager
params:
placeholder_strings: ["*"]
initializer_words: ['sculpture']
per_image_tokens: false
num_vectors_per_token: 1
progressive_words: False
unet_config:
target: invokeai.backend.stable_diffusion.diffusionmodules.openaimodel.UNetModel
params:
image_size: 32 # unused
in_channels: 4
out_channels: 4
model_channels: 320
attention_resolutions: [ 4, 2, 1 ]
num_res_blocks: 2
channel_mult: [ 1, 2, 4, 4 ]
num_heads: 8
use_spatial_transformer: True
transformer_depth: 1
context_dim: 768
use_checkpoint: True
legacy: False
first_stage_config:
target: invokeai.backend.stable_diffusion.autoencoder.AutoencoderKL
params:
embed_dim: 4
monitor: val/rec_loss
ddconfig:
double_z: true
z_channels: 4
resolution: 256
in_channels: 3
out_ch: 3
ch: 128
ch_mult:
- 1
- 2
- 4
- 4
num_res_blocks: 2
attn_resolutions: []
dropout: 0.0
lossconfig:
target: torch.nn.Identity
cond_stage_config:
target: invokeai.backend.stable_diffusion.encoders.modules.WeightedFrozenCLIPEmbedder
@@ -0,0 +1 @@
This is a template empty invokeai root directory used to test model management.
@@ -0,0 +1 @@
This is a template empty invokeai root directory used to test model management.
@@ -0,0 +1,34 @@
{
"_class_name": "StableDiffusionXLPipeline",
"_diffusers_version": "0.23.0",
"_name_or_path": "stabilityai/sdxl-turbo",
"force_zeros_for_empty_prompt": true,
"scheduler": [
"diffusers",
"EulerAncestralDiscreteScheduler"
],
"text_encoder": [
"transformers",
"CLIPTextModel"
],
"text_encoder_2": [
"transformers",
"CLIPTextModelWithProjection"
],
"tokenizer": [
"transformers",
"CLIPTokenizer"
],
"tokenizer_2": [
"transformers",
"CLIPTokenizer"
],
"unet": [
"diffusers",
"UNet2DConditionModel"
],
"vae": [
"diffusers",
"AutoencoderKL"
]
}
@@ -0,0 +1,17 @@
{
"_class_name": "EulerAncestralDiscreteScheduler",
"_diffusers_version": "0.23.0",
"beta_end": 0.012,
"beta_schedule": "scaled_linear",
"beta_start": 0.00085,
"clip_sample": false,
"interpolation_type": "linear",
"num_train_timesteps": 1000,
"prediction_type": "epsilon",
"sample_max_value": 1.0,
"set_alpha_to_one": false,
"skip_prk_steps": true,
"steps_offset": 1,
"timestep_spacing": "trailing",
"trained_betas": null
}
@@ -0,0 +1,25 @@
{
"_name_or_path": "/home/lstein/.cache/huggingface/hub/models--stabilityai--sdxl-turbo/snapshots/fbda35297a8280789ffe2e25206800702fa5c4c1/text_encoder",
"architectures": [
"CLIPTextModel"
],
"attention_dropout": 0.0,
"bos_token_id": 0,
"dropout": 0.0,
"eos_token_id": 2,
"hidden_act": "quick_gelu",
"hidden_size": 768,
"initializer_factor": 1.0,
"initializer_range": 0.02,
"intermediate_size": 3072,
"layer_norm_eps": 1e-05,
"max_position_embeddings": 77,
"model_type": "clip_text_model",
"num_attention_heads": 12,
"num_hidden_layers": 12,
"pad_token_id": 1,
"projection_dim": 768,
"torch_dtype": "float16",
"transformers_version": "4.35.0",
"vocab_size": 49408
}
@@ -0,0 +1,25 @@
{
"_name_or_path": "/home/lstein/.cache/huggingface/hub/models--stabilityai--sdxl-turbo/snapshots/fbda35297a8280789ffe2e25206800702fa5c4c1/text_encoder_2",
"architectures": [
"CLIPTextModelWithProjection"
],
"attention_dropout": 0.0,
"bos_token_id": 0,
"dropout": 0.0,
"eos_token_id": 2,
"hidden_act": "gelu",
"hidden_size": 1280,
"initializer_factor": 1.0,
"initializer_range": 0.02,
"intermediate_size": 5120,
"layer_norm_eps": 1e-05,
"max_position_embeddings": 77,
"model_type": "clip_text_model",
"num_attention_heads": 20,
"num_hidden_layers": 32,
"pad_token_id": 1,
"projection_dim": 1280,
"torch_dtype": "float16",
"transformers_version": "4.35.0",
"vocab_size": 49408
}
@@ -0,0 +1,30 @@
{
"bos_token": {
"content": "<|startoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
},
"eos_token": {
"content": "<|endoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
},
"pad_token": {
"content": "<|endoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
},
"unk_token": {
"content": "<|endoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
}
}
@@ -0,0 +1,30 @@
{
"add_prefix_space": false,
"added_tokens_decoder": {
"49406": {
"content": "<|startoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false,
"special": true
},
"49407": {
"content": "<|endoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false,
"special": true
}
},
"bos_token": "<|startoftext|>",
"clean_up_tokenization_spaces": true,
"do_lower_case": true,
"eos_token": "<|endoftext|>",
"errors": "replace",
"model_max_length": 77,
"pad_token": "<|endoftext|>",
"tokenizer_class": "CLIPTokenizer",
"unk_token": "<|endoftext|>"
}
@@ -0,0 +1,30 @@
{
"bos_token": {
"content": "<|startoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
},
"eos_token": {
"content": "<|endoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
},
"pad_token": {
"content": "!",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false
},
"unk_token": {
"content": "<|endoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
}
}
@@ -0,0 +1,38 @@
{
"add_prefix_space": false,
"added_tokens_decoder": {
"0": {
"content": "!",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"49406": {
"content": "<|startoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false,
"special": true
},
"49407": {
"content": "<|endoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false,
"special": true
}
},
"bos_token": "<|startoftext|>",
"clean_up_tokenization_spaces": true,
"do_lower_case": true,
"eos_token": "<|endoftext|>",
"errors": "replace",
"model_max_length": 77,
"pad_token": "!",
"tokenizer_class": "CLIPTokenizer",
"unk_token": "<|endoftext|>"
}
@@ -0,0 +1,73 @@
{
"_class_name": "UNet2DConditionModel",
"_diffusers_version": "0.23.0",
"_name_or_path": "/home/lstein/.cache/huggingface/hub/models--stabilityai--sdxl-turbo/snapshots/fbda35297a8280789ffe2e25206800702fa5c4c1/unet",
"act_fn": "silu",
"addition_embed_type": "text_time",
"addition_embed_type_num_heads": 64,
"addition_time_embed_dim": 256,
"attention_head_dim": [
5,
10,
20
],
"attention_type": "default",
"block_out_channels": [
320,
640,
1280
],
"center_input_sample": false,
"class_embed_type": null,
"class_embeddings_concat": false,
"conv_in_kernel": 3,
"conv_out_kernel": 3,
"cross_attention_dim": 2048,
"cross_attention_norm": null,
"down_block_types": [
"DownBlock2D",
"CrossAttnDownBlock2D",
"CrossAttnDownBlock2D"
],
"downsample_padding": 1,
"dropout": 0.0,
"dual_cross_attention": false,
"encoder_hid_dim": null,
"encoder_hid_dim_type": null,
"flip_sin_to_cos": true,
"freq_shift": 0,
"in_channels": 4,
"layers_per_block": 2,
"mid_block_only_cross_attention": null,
"mid_block_scale_factor": 1,
"mid_block_type": "UNetMidBlock2DCrossAttn",
"norm_eps": 1e-05,
"norm_num_groups": 32,
"num_attention_heads": null,
"num_class_embeds": null,
"only_cross_attention": false,
"out_channels": 4,
"projection_class_embeddings_input_dim": 2816,
"resnet_out_scale_factor": 1.0,
"resnet_skip_time_act": false,
"resnet_time_scale_shift": "default",
"reverse_transformer_layers_per_block": null,
"sample_size": 64,
"time_cond_proj_dim": null,
"time_embedding_act_fn": null,
"time_embedding_dim": null,
"time_embedding_type": "positional",
"timestep_post_act": null,
"transformer_layers_per_block": [
1,
2,
10
],
"up_block_types": [
"CrossAttnUpBlock2D",
"CrossAttnUpBlock2D",
"UpBlock2D"
],
"upcast_attention": null,
"use_linear_projection": true
}
@@ -0,0 +1,32 @@
{
"_class_name": "AutoencoderKL",
"_diffusers_version": "0.23.0",
"_name_or_path": "/home/lstein/.cache/huggingface/hub/models--stabilityai--sdxl-turbo/snapshots/fbda35297a8280789ffe2e25206800702fa5c4c1/vae",
"act_fn": "silu",
"block_out_channels": [
128,
256,
512,
512
],
"down_block_types": [
"DownEncoderBlock2D",
"DownEncoderBlock2D",
"DownEncoderBlock2D",
"DownEncoderBlock2D"
],
"force_upcast": true,
"in_channels": 3,
"latent_channels": 4,
"layers_per_block": 2,
"norm_num_groups": 32,
"out_channels": 3,
"sample_size": 1024,
"scaling_factor": 0.13025,
"up_block_types": [
"UpDecoderBlock2D",
"UpDecoderBlock2D",
"UpDecoderBlock2D",
"UpDecoderBlock2D"
]
}
@@ -0,0 +1,143 @@
import torch
from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_only_full_load import (
CachedModelOnlyFullLoad,
)
from tests.backend.model_manager.load.model_cache.cached_model.utils import (
DummyModule,
parameterize_keep_ram_copy,
parameterize_mps_and_cuda,
)
class NonTorchModel:
"""A model that does not sub-class torch.nn.Module."""
def __init__(self):
self.linear = torch.nn.Linear(10, 32)
def run_inference(self, x: torch.Tensor) -> torch.Tensor:
return self.linear(x)
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_total_bytes(device: str, keep_ram_copy: bool):
model = DummyModule()
cached_model = CachedModelOnlyFullLoad(
model=model, compute_device=torch.device(device), total_bytes=100, keep_ram_copy=keep_ram_copy
)
assert cached_model.total_bytes() == 100
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_is_in_vram(device: str, keep_ram_copy: bool):
model = DummyModule()
cached_model = CachedModelOnlyFullLoad(
model=model, compute_device=torch.device(device), total_bytes=100, keep_ram_copy=keep_ram_copy
)
assert not cached_model.is_in_vram()
assert cached_model.cur_vram_bytes() == 0
cached_model.full_load_to_vram()
assert cached_model.is_in_vram()
assert cached_model.cur_vram_bytes() == 100
cached_model.full_unload_from_vram()
assert not cached_model.is_in_vram()
assert cached_model.cur_vram_bytes() == 0
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_full_load_and_unload(device: str, keep_ram_copy: bool):
model = DummyModule()
cached_model = CachedModelOnlyFullLoad(
model=model, compute_device=torch.device(device), total_bytes=100, keep_ram_copy=keep_ram_copy
)
assert cached_model.full_load_to_vram() == 100
assert cached_model.is_in_vram()
assert all(p.device.type == device for p in cached_model.model.parameters())
assert cached_model.full_unload_from_vram() == 100
assert not cached_model.is_in_vram()
assert all(p.device.type == "cpu" for p in cached_model.model.parameters())
@parameterize_mps_and_cuda
def test_cached_model_get_cpu_state_dict(device: str):
model = DummyModule()
cached_model = CachedModelOnlyFullLoad(
model=model, compute_device=torch.device(device), total_bytes=100, keep_ram_copy=True
)
assert not cached_model.is_in_vram()
# The CPU state dict can be accessed and has the expected properties.
cpu_state_dict = cached_model.get_cpu_state_dict()
assert cpu_state_dict is not None
assert len(cpu_state_dict) == len(model.state_dict())
assert all(p.device.type == "cpu" for p in cpu_state_dict.values())
# Full load the model into VRAM.
cached_model.full_load_to_vram()
assert cached_model.is_in_vram()
# The CPU state dict is still available, and still on the CPU.
cpu_state_dict = cached_model.get_cpu_state_dict()
assert cpu_state_dict is not None
assert len(cpu_state_dict) == len(model.state_dict())
assert all(p.device.type == "cpu" for p in cpu_state_dict.values())
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_full_load_and_inference(device: str, keep_ram_copy: bool):
model = DummyModule()
cached_model = CachedModelOnlyFullLoad(
model=model, compute_device=torch.device(device), total_bytes=100, keep_ram_copy=keep_ram_copy
)
assert not cached_model.is_in_vram()
# Run inference on the CPU.
x = torch.randn(1, 10)
output1 = model(x)
assert output1.device.type == "cpu"
# Full load the model into VRAM.
cached_model.full_load_to_vram()
assert cached_model.is_in_vram()
# Run inference on the GPU.
output2 = model(x.to(device))
assert output2.device.type == device
# The outputs should be the same for both runs.
assert torch.allclose(output1, output2.to("cpu"))
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_non_torch_model(device: str, keep_ram_copy: bool):
model = NonTorchModel()
cached_model = CachedModelOnlyFullLoad(
model=model, compute_device=torch.device(device), total_bytes=100, keep_ram_copy=keep_ram_copy
)
assert not cached_model.is_in_vram()
# The model does not have a CPU state dict.
assert cached_model.get_cpu_state_dict() is None
# Attempting to load the model into VRAM should have no effect.
cached_model.full_load_to_vram()
assert not cached_model.is_in_vram()
assert cached_model.cur_vram_bytes() == 0
# Attempting to unload the model from VRAM should have no effect.
cached_model.full_unload_from_vram()
assert not cached_model.is_in_vram()
assert cached_model.cur_vram_bytes() == 0
# Running inference on the CPU should work.
output1 = model.run_inference(torch.randn(1, 10))
assert output1.device.type == "cpu"
@@ -0,0 +1,341 @@
import itertools
import pytest
import torch
from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_with_partial_load import (
CachedModelWithPartialLoad,
)
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import (
apply_custom_layers_to_model,
)
from invokeai.backend.util.calc_tensor_size import calc_tensor_size
from tests.backend.model_manager.load.model_cache.cached_model.utils import (
DummyModule,
parameterize_keep_ram_copy,
parameterize_mps_and_cuda,
)
@pytest.fixture
def model():
model = DummyModule()
apply_custom_layers_to_model(model)
return model
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_total_bytes(device: str, model: DummyModule, keep_ram_copy: bool):
cached_model = CachedModelWithPartialLoad(
model=model, compute_device=torch.device(device), keep_ram_copy=keep_ram_copy
)
linear1_numel = 10 * 32 + 32
linear2_numel = 32 * 64 + 64
buffer1_numel = 64
# Note that the non-persistent buffer (buffer2) is not included in .total_bytes() calculation.
assert cached_model.total_bytes() == (linear1_numel + linear2_numel + buffer1_numel) * 4
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_cur_vram_bytes(device: str, model: DummyModule, keep_ram_copy: bool):
# Model starts in CPU memory.
cached_model = CachedModelWithPartialLoad(
model=model, compute_device=torch.device(device), keep_ram_copy=keep_ram_copy
)
assert cached_model.cur_vram_bytes() == 0
# Full load the model into VRAM.
cached_model.full_load_to_vram()
assert cached_model.cur_vram_bytes() > 0
assert cached_model.cur_vram_bytes() == cached_model.total_bytes()
assert all(p.device.type == device for p in model.parameters())
assert all(p.device.type == device for p in model.buffers())
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_partial_load(device: str, model: DummyModule, keep_ram_copy: bool):
# Model starts in CPU memory.
cached_model = CachedModelWithPartialLoad(
model=model, compute_device=torch.device(device), keep_ram_copy=keep_ram_copy
)
model_total_bytes = cached_model.total_bytes()
assert cached_model.cur_vram_bytes() == 0
# Partially load the model into VRAM.
target_vram_bytes = int(model_total_bytes * 0.6)
loaded_bytes = cached_model.partial_load_to_vram(target_vram_bytes)
# Check that the model is partially loaded into VRAM.
assert loaded_bytes > 0
assert loaded_bytes < model_total_bytes
assert loaded_bytes == cached_model.cur_vram_bytes()
assert loaded_bytes == sum(
calc_tensor_size(p)
for n, p in itertools.chain(model.named_parameters(), model.named_buffers())
if p.device.type == device and n != "buffer2"
)
# Check that the model's modules have device autocasting enabled.
assert model.linear1.is_device_autocasting_enabled()
assert model.linear2.is_device_autocasting_enabled()
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_partial_unload(device: str, model: DummyModule, keep_ram_copy: bool):
# Model starts in CPU memory.
cached_model = CachedModelWithPartialLoad(
model=model, compute_device=torch.device(device), keep_ram_copy=keep_ram_copy
)
model_total_bytes = cached_model.total_bytes()
assert cached_model.cur_vram_bytes() == 0
# Full load the model into VRAM.
cached_model.full_load_to_vram()
assert cached_model.cur_vram_bytes() == model_total_bytes
# Partially unload the model from VRAM.
bytes_to_free = int(model_total_bytes * 0.4)
freed_bytes = cached_model.partial_unload_from_vram(bytes_to_free)
# Check that the model is partially unloaded from VRAM.
assert freed_bytes >= bytes_to_free
assert freed_bytes < model_total_bytes
assert freed_bytes == model_total_bytes - cached_model.cur_vram_bytes()
assert freed_bytes == sum(
calc_tensor_size(p) for p in itertools.chain(model.parameters(), model.buffers()) if p.device.type == "cpu"
)
# Check that the model's modules still have device autocasting enabled.
assert model.linear1.is_device_autocasting_enabled()
assert model.linear2.is_device_autocasting_enabled()
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_partial_unload_keep_required_weights_in_vram(
device: str, model: DummyModule, keep_ram_copy: bool
):
# Model starts in CPU memory.
cached_model = CachedModelWithPartialLoad(
model=model, compute_device=torch.device(device), keep_ram_copy=keep_ram_copy
)
model_total_bytes = cached_model.total_bytes()
assert cached_model.cur_vram_bytes() == 0
# Full load the model into VRAM.
cached_model.full_load_to_vram()
assert cached_model.cur_vram_bytes() == model_total_bytes
# Partially unload the model from VRAM, but request the required weights to be kept in VRAM.
bytes_to_free = int(model_total_bytes)
freed_bytes = cached_model.partial_unload_from_vram(bytes_to_free, keep_required_weights_in_vram=True)
# Check that the model is partially unloaded from VRAM.
assert freed_bytes < model_total_bytes
assert freed_bytes == model_total_bytes - cached_model.cur_vram_bytes()
assert freed_bytes == sum(
calc_tensor_size(p) for p in itertools.chain(model.parameters(), model.buffers()) if p.device.type == "cpu"
)
# The parameters should be offloaded to the CPU, because they are in Linear layers.
assert all(p.device.type == "cpu" for p in model.parameters())
# The buffer should still be on the device, because it is in a layer that does not support autocast.
assert all(p.device.type == device for p in model.buffers())
# Check that the model's modules still have device autocasting enabled.
assert model.linear1.is_device_autocasting_enabled()
assert model.linear2.is_device_autocasting_enabled()
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_full_load_and_unload(device: str, model: DummyModule, keep_ram_copy: bool):
cached_model = CachedModelWithPartialLoad(
model=model, compute_device=torch.device(device), keep_ram_copy=keep_ram_copy
)
# Model starts in CPU memory.
model_total_bytes = cached_model.total_bytes()
assert cached_model.cur_vram_bytes() == 0
# Full load the model into VRAM.
loaded_bytes = cached_model.full_load_to_vram()
assert loaded_bytes > 0
assert loaded_bytes == model_total_bytes
assert loaded_bytes == cached_model.cur_vram_bytes()
assert all(p.device.type == device for p in itertools.chain(model.parameters(), model.buffers()))
assert not model.linear1.is_device_autocasting_enabled()
assert not model.linear2.is_device_autocasting_enabled()
# Full unload the model from VRAM.
unloaded_bytes = cached_model.full_unload_from_vram()
# Check that the model is fully unloaded from VRAM.
assert unloaded_bytes > 0
assert unloaded_bytes == model_total_bytes
assert cached_model.cur_vram_bytes() == 0
# Note that the non-persistent buffer (buffer2) is not required to be unloaded from VRAM.
assert all(
p.device.type == "cpu"
for n, p in itertools.chain(model.named_parameters(), model.named_buffers())
if n != "buffer2"
)
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_full_load_from_partial(device: str, model: DummyModule, keep_ram_copy: bool):
cached_model = CachedModelWithPartialLoad(
model=model, compute_device=torch.device(device), keep_ram_copy=keep_ram_copy
)
# Model starts in CPU memory.
model_total_bytes = cached_model.total_bytes()
assert cached_model.cur_vram_bytes() == 0
# Partially load the model into VRAM.
target_vram_bytes = int(model_total_bytes * 0.6)
loaded_bytes = cached_model.partial_load_to_vram(target_vram_bytes)
assert loaded_bytes > 0
assert loaded_bytes < model_total_bytes
assert loaded_bytes == cached_model.cur_vram_bytes()
assert model.linear1.is_device_autocasting_enabled()
assert model.linear2.is_device_autocasting_enabled()
# Full load the rest of the model into VRAM.
loaded_bytes_2 = cached_model.full_load_to_vram()
assert loaded_bytes_2 > 0
assert loaded_bytes_2 < model_total_bytes
assert loaded_bytes + loaded_bytes_2 == cached_model.cur_vram_bytes()
assert loaded_bytes + loaded_bytes_2 == model_total_bytes
assert all(p.device.type == device for p in itertools.chain(model.parameters(), model.buffers()))
assert not model.linear1.is_device_autocasting_enabled()
assert not model.linear2.is_device_autocasting_enabled()
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_full_unload_from_partial(device: str, model: DummyModule, keep_ram_copy: bool):
cached_model = CachedModelWithPartialLoad(
model=model, compute_device=torch.device(device), keep_ram_copy=keep_ram_copy
)
# Model starts in CPU memory.
model_total_bytes = cached_model.total_bytes()
assert cached_model.cur_vram_bytes() == 0
# Partially load the model into VRAM.
target_vram_bytes = int(model_total_bytes * 0.6)
loaded_bytes = cached_model.partial_load_to_vram(target_vram_bytes)
assert loaded_bytes > 0
assert loaded_bytes < model_total_bytes
assert loaded_bytes == cached_model.cur_vram_bytes()
# Full unload the model from VRAM.
unloaded_bytes = cached_model.full_unload_from_vram()
assert unloaded_bytes > 0
assert unloaded_bytes == loaded_bytes
assert cached_model.cur_vram_bytes() == 0
# Note that the non-persistent buffer (buffer2) is not required to be unloaded from VRAM.
assert all(
p.device.type == "cpu"
for n, p in itertools.chain(model.named_parameters(), model.named_buffers())
if n != "buffer2"
)
@parameterize_mps_and_cuda
def test_cached_model_get_cpu_state_dict(device: str, model: DummyModule):
cached_model = CachedModelWithPartialLoad(model=model, compute_device=torch.device(device), keep_ram_copy=True)
# Model starts in CPU memory.
assert cached_model.cur_vram_bytes() == 0
# The CPU state dict can be accessed and has the expected properties.
cpu_state_dict = cached_model.get_cpu_state_dict()
assert cpu_state_dict is not None
assert len(cpu_state_dict) == len(model.state_dict())
assert all(p.device.type == "cpu" for p in cpu_state_dict.values())
# Full load the model into VRAM.
cached_model.full_load_to_vram()
assert cached_model.cur_vram_bytes() == cached_model.total_bytes()
# The CPU state dict is still available, and still on the CPU.
cpu_state_dict = cached_model.get_cpu_state_dict()
assert cpu_state_dict is not None
assert len(cpu_state_dict) == len(model.state_dict())
assert all(p.device.type == "cpu" for p in cpu_state_dict.values())
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_full_load_and_inference(device: str, model: DummyModule, keep_ram_copy: bool):
cached_model = CachedModelWithPartialLoad(
model=model, compute_device=torch.device(device), keep_ram_copy=keep_ram_copy
)
# Model starts in CPU memory.
model_total_bytes = cached_model.total_bytes()
assert cached_model.cur_vram_bytes() == 0
# Run inference on the CPU.
x = torch.randn(1, 10)
output1 = model(x)
assert output1.device.type == "cpu"
# Full load the model into VRAM.
loaded_bytes = cached_model.full_load_to_vram()
assert loaded_bytes > 0
assert loaded_bytes == model_total_bytes
assert loaded_bytes == cached_model.cur_vram_bytes()
assert all(p.device.type == device for p in itertools.chain(model.parameters(), model.buffers()))
# Run inference on the GPU.
output2 = model(x.to(device))
assert output2.device.type == device
# The outputs should be the same for both runs.
assert torch.allclose(output1, output2.to("cpu"))
@parameterize_mps_and_cuda
@parameterize_keep_ram_copy
def test_cached_model_partial_load_and_inference(device: str, model: DummyModule, keep_ram_copy: bool):
# Model starts in CPU memory.
cached_model = CachedModelWithPartialLoad(
model=model, compute_device=torch.device(device), keep_ram_copy=keep_ram_copy
)
model_total_bytes = cached_model.total_bytes()
assert cached_model.cur_vram_bytes() == 0
# Run inference on the CPU.
x = torch.randn(1, 10)
output1 = model(x)
assert output1.device.type == "cpu"
# Partially load the model into VRAM.
target_vram_bytes = int(model_total_bytes * 0.6)
loaded_bytes = cached_model.partial_load_to_vram(target_vram_bytes)
# Check that the model is partially loaded into VRAM.
assert loaded_bytes > 0
assert loaded_bytes < model_total_bytes
assert loaded_bytes == cached_model.cur_vram_bytes()
assert loaded_bytes == sum(
calc_tensor_size(p)
for n, p in itertools.chain(model.named_parameters(), model.named_buffers())
if p.device.type == device and n != "buffer2"
)
# Check that the model's modules have device autocasting enabled.
assert model.linear1.is_device_autocasting_enabled()
assert model.linear2.is_device_autocasting_enabled()
# Run inference on the GPU.
output2 = model(x.to(device))
assert output2.device.type == device
# The output should be the same as the output from the CPU.
assert torch.allclose(output1, output2.to("cpu"))
@@ -0,0 +1,47 @@
import pytest
import torch
from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_with_partial_load import (
CachedModelWithPartialLoad,
)
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import (
apply_custom_layers_to_model,
)
class ModelWithRequiredScale(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear = torch.nn.Linear(4, 4)
self.scale = torch.nn.Parameter(torch.ones(4))
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.linear(x) * self.scale
@pytest.mark.parametrize(
"device",
[
pytest.param(
torch.device("cuda"), marks=pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA device")
),
pytest.param(
torch.device("mps"),
marks=pytest.mark.skipif(not torch.backends.mps.is_available(), reason="requires MPS device"),
),
],
)
@pytest.mark.parametrize("keep_ram_copy", [True, False])
@torch.no_grad()
def test_repair_required_tensors_on_compute_device(device: torch.device, keep_ram_copy: bool):
model = ModelWithRequiredScale()
apply_custom_layers_to_model(model, device_autocasting_enabled=True)
cached_model = CachedModelWithPartialLoad(model=model, compute_device=device, keep_ram_copy=keep_ram_copy)
cached_model._cur_vram_bytes = 0
repaired_tensors = cached_model.repair_required_tensors_on_compute_device()
assert repaired_tensors == 1
assert cached_model._cur_vram_bytes is None
assert model.scale.device.type == device.type
assert all(param.device.type == "cpu" for param in model.linear.parameters())
@@ -0,0 +1,41 @@
import os
import pytest
import torch
class DummyModule(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear1 = torch.nn.Linear(10, 32)
self.linear2 = torch.nn.Linear(32, 64)
self.register_buffer("buffer1", torch.ones(64))
# Non-persistent buffers are not included in the state dict. We need to make sure that this case is handled
# correctly by the partial loading code.
self.register_buffer("buffer2", torch.ones(64), persistent=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.linear1(x)
x = self.linear2(x)
x = x + self.buffer1
x = x + self.buffer2
return x
is_github_ci = os.getenv("GITHUB_ACTIONS") == "true"
parameterize_mps_and_cuda = pytest.mark.parametrize(
("device"),
[
pytest.param(
"mps",
marks=pytest.mark.skipif(
is_github_ci or not torch.backends.mps.is_available(),
reason="MPS is very flaky in CI" if is_github_ci else "MPS is not available.",
),
),
pytest.param("cuda", marks=pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is not available.")),
],
)
parameterize_keep_ram_copy = pytest.mark.parametrize("keep_ram_copy", [True, False])
@@ -0,0 +1,224 @@
"""Tests for `ModelCache.drop_model` — used by the model_manager API to invalidate cached
entries when a setting that changes how a model loads (e.g. `fp8_storage`, `cpu_only`) is
toggled. Without this, the toggle is silently a no-op until the entry is evicted by other
means (clear cache, eviction under memory pressure, restart).
Also covers:
- Locked entries are marked stale and evicted by `unlock()` — without that, a setting toggled
during an in-flight generation would survive on the locked entry and silently be reused.
- `stats.cleared` and the `cleared` callbacks fire on invalidation, mirroring the eviction
path through `_make_room_internal`, so observers and stats stay accurate.
"""
import logging
from unittest.mock import MagicMock
import pytest
import torch
from invokeai.backend.model_manager.load.model_cache.cache_stats import CacheStats
from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache
@pytest.fixture
def mock_logger():
logger = MagicMock()
logger.getEffectiveLevel.return_value = logging.INFO
return logger
@pytest.fixture
def cache(mock_logger):
cache = ModelCache(
execution_device_working_mem_gb=1.0,
enable_partial_loading=False,
keep_ram_copy_of_weights=True,
execution_device="cpu",
storage_device="cpu",
logger=mock_logger,
)
yield cache
cache.shutdown()
def test_drop_model_removes_all_submodel_entries(cache: ModelCache):
"""A model with multiple submodels has multiple cache keys (`<key>` and `<key>:<submodel>`);
drop_model must drop them all together so the next load rebuilds with the new settings.
"""
model_key = "abc123"
cache.put(model_key, torch.randn(4))
cache.put(f"{model_key}:unet", torch.randn(4))
cache.put(f"{model_key}:text_encoder", torch.randn(4))
cache.put("other_model", torch.randn(4))
cache.put("other_model:unet", torch.randn(4))
dropped = cache.drop_model(model_key)
assert dropped == 3
assert model_key not in cache._cached_models
assert f"{model_key}:unet" not in cache._cached_models
assert f"{model_key}:text_encoder" not in cache._cached_models
# Unrelated model is left alone.
assert "other_model" in cache._cached_models
assert "other_model:unet" in cache._cached_models
def test_drop_model_marks_locked_entries_stale_without_evicting(cache: ModelCache):
"""Locked entries are in active use; we must not yank them out from under inference.
But we also must not silently retain them after the lock releases — otherwise a setting
toggle that happened during inference would survive and the next generation would reuse
the pre-change cached module. drop_model marks locked entries `is_stale=True`; unlock
evicts them as soon as the last lock releases.
"""
model_key = "abc123"
cache.put(model_key, torch.randn(4))
cache.put(f"{model_key}:unet", torch.randn(4))
locked_entry = cache._cached_models[f"{model_key}:unet"]
locked_entry.lock()
dropped = cache.drop_model(model_key)
assert dropped == 1
assert model_key not in cache._cached_models
assert f"{model_key}:unet" in cache._cached_models
assert locked_entry.is_stale is True
def test_unlock_evicts_stale_entry(cache: ModelCache):
"""The flip side of `marks_locked_entries_stale`: the next `unlock` after a stale-marking
invalidation must actually remove the entry.
"""
model_key = "abc123"
cache.put(model_key, torch.randn(4))
entry = cache._cached_models[model_key]
entry.lock()
cache.drop_model(model_key)
# Entry still here while locked.
assert model_key in cache._cached_models
assert entry.is_stale is True
cache.unlock(entry)
assert model_key not in cache._cached_models
def test_unlock_does_not_evict_non_stale_entry(cache: ModelCache):
"""The stale-eviction path must not affect ordinary unlock — only stale-marked entries
should be evicted on unlock.
"""
model_key = "abc123"
cache.put(model_key, torch.randn(4))
entry = cache._cached_models[model_key]
entry.lock()
cache.unlock(entry)
# No drop_model was called, so entry should still be there.
assert model_key in cache._cached_models
def test_unlock_only_evicts_when_last_lock_releases(cache: ModelCache):
"""If the entry is held by multiple locks (the cache supports re-entrant locking via
`_locks`), eviction must wait until they all release. Otherwise we'd yank the entry out
from under a caller that still expects it loaded.
"""
model_key = "abc123"
cache.put(model_key, torch.randn(4))
entry = cache._cached_models[model_key]
entry.lock()
entry.lock()
cache.drop_model(model_key)
assert entry.is_stale is True
cache.unlock(entry)
# Still locked by one holder — must remain.
assert model_key in cache._cached_models
cache.unlock(entry)
# Now fully released — eviction happens.
assert model_key not in cache._cached_models
def test_drop_model_updates_stats_and_fires_callbacks(cache: ModelCache):
"""drop_model is a real eviction path — observers watching for cache changes (stats,
cleared callbacks) must see it just like the make_room eviction path. Otherwise the UI
cache-stats panel and any external observer would miss invalidations.
"""
model_key = "abc123"
# Use real nn.Modules so `total_bytes()` is non-zero (raw tensors are sized as 0 by
# `calc_model_size_by_data` since the cache doesn't know what they are).
cache.put(model_key, torch.nn.Linear(4, 4))
cache.put(f"{model_key}:unet", torch.nn.Linear(4, 4))
cache.stats = CacheStats()
callback = MagicMock()
cache.on_cache_models_cleared(callback)
dropped = cache.drop_model(model_key)
assert dropped == 2
assert cache.stats.cleared == 2
callback.assert_called_once()
kwargs = callback.call_args.kwargs
assert kwargs["models_cleared"] == 2
assert kwargs["bytes_requested"] == 0 # not a make-room call
assert kwargs["bytes_freed"] > 0
def test_unlock_stale_eviction_updates_stats_and_fires_callbacks(cache: ModelCache):
"""Stale-entry eviction is also a cache change observers care about."""
model_key = "abc123"
cache.put(model_key, torch.randn(4))
entry = cache._cached_models[model_key]
entry.lock()
cache.drop_model(model_key)
cache.stats = CacheStats()
callback = MagicMock()
cache.on_cache_models_cleared(callback)
cache.unlock(entry)
assert model_key not in cache._cached_models
assert cache.stats.cleared == 1
callback.assert_called_once()
def test_drop_model_with_no_matches_does_not_fire_callbacks(cache: ModelCache):
"""No-op invalidations should be silent — don't spam observers with empty events."""
cache.put("other_model", torch.randn(4))
callback = MagicMock()
cache.on_cache_models_cleared(callback)
dropped = cache.drop_model("does_not_exist")
assert dropped == 0
callback.assert_not_called()
def test_drop_model_with_no_matches_is_noop(cache: ModelCache):
cache.put("other_model", torch.randn(4))
dropped = cache.drop_model("does_not_exist")
assert dropped == 0
assert "other_model" in cache._cached_models
def test_drop_model_does_not_match_prefix_substring(cache: ModelCache):
"""`drop_model("abc")` must not drop `abcd` — only the exact key or `abc:<submodel>`."""
cache.put("abc", torch.randn(4))
cache.put("abcd", torch.randn(4))
cache.put("abc:unet", torch.randn(4))
dropped = cache.drop_model("abc")
assert dropped == 2
assert "abc" not in cache._cached_models
assert "abc:unet" not in cache._cached_models
assert "abcd" in cache._cached_models
@@ -0,0 +1,126 @@
"""Tests for model cache keep-alive timeout functionality."""
import logging
import time
from unittest.mock import MagicMock
import pytest
import torch
from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache
@pytest.fixture
def mock_logger():
"""Create a mock logger."""
logger = MagicMock()
# Configure the mock to return a valid log level for getEffectiveLevel()
logger.getEffectiveLevel.return_value = logging.INFO
return logger
@pytest.fixture
def model_cache_with_timeout(mock_logger):
"""Create a ModelCache instance with a short timeout for testing."""
cache = ModelCache(
execution_device_working_mem_gb=1.0,
enable_partial_loading=False,
keep_ram_copy_of_weights=True,
execution_device="cpu",
storage_device="cpu",
logger=mock_logger,
keep_alive_minutes=0.01, # 0.6 seconds for fast testing
)
yield cache
cache.shutdown()
@pytest.fixture
def model_cache_no_timeout(mock_logger):
"""Create a ModelCache instance without timeout (default behavior)."""
cache = ModelCache(
execution_device_working_mem_gb=1.0,
enable_partial_loading=False,
keep_ram_copy_of_weights=True,
execution_device="cpu",
storage_device="cpu",
logger=mock_logger,
keep_alive_minutes=0, # 0 means no timeout
)
yield cache
cache.shutdown()
def test_timeout_clears_cache(model_cache_with_timeout):
"""Test that the cache is cleared after the timeout expires."""
cache = model_cache_with_timeout
# Add a simple tensor to the cache
test_tensor = torch.randn(10, 10)
cache.put("test_model", test_tensor)
# Verify the model is in the cache
assert "test_model" in cache._cached_models
# Wait for the timeout to expire (0.01 minutes = 0.6 seconds + buffer)
time.sleep(1.5)
# Verify the cache has been cleared
assert len(cache._cached_models) == 0
def test_activity_resets_timeout(model_cache_with_timeout):
"""Test that model activity resets the timeout."""
cache = model_cache_with_timeout
# Add a simple tensor to the cache
test_tensor = torch.randn(10, 10)
cache.put("test_model", test_tensor)
# Wait half the timeout
time.sleep(0.4)
# Access the model to reset the timeout
cache.get("test_model")
# Wait another half timeout (model should still be in cache)
time.sleep(0.4)
# Verify the model is still in the cache
assert "test_model" in cache._cached_models
def test_no_timeout_keeps_models(model_cache_no_timeout):
"""Test that models are kept indefinitely when timeout is 0."""
cache = model_cache_no_timeout
# Add a simple tensor to the cache
test_tensor = torch.randn(10, 10)
cache.put("test_model", test_tensor)
# Verify the model is in the cache
assert "test_model" in cache._cached_models
# Wait longer than what would be a timeout
time.sleep(1.0)
# Verify the model is still in the cache
assert "test_model" in cache._cached_models
def test_shutdown_cancels_timer(model_cache_with_timeout):
"""Test that shutdown properly cancels the timeout timer."""
cache = model_cache_with_timeout
# Add a model to start the timer
test_tensor = torch.randn(10, 10)
cache.put("test_model", test_tensor)
# Shutdown the cache
cache.shutdown()
# Wait for what would be the timeout
time.sleep(1.0)
# The model should still be in the cache since shutdown was called
assert "test_model" in cache._cached_models
@@ -0,0 +1,763 @@
import copy
from collections.abc import Callable
import gguf
import pytest
import torch
from invokeai.backend.flux.modules.layers import RMSNorm
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import (
AUTOCAST_MODULE_TYPE_MAPPING,
AUTOCAST_MODULE_TYPE_MAPPING_INVERSE,
unwrap_custom_layer,
wrap_custom_layer,
)
from invokeai.backend.patches.layer_patcher import LayerPatcher
from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch
from invokeai.backend.patches.layers.dora_layer import DoRALayer
from invokeai.backend.patches.layers.flux_control_lora_layer import FluxControlLoRALayer
from invokeai.backend.patches.layers.lokr_layer import LoKRLayer
from invokeai.backend.patches.layers.lora_layer import LoRALayer
from invokeai.backend.patches.layers.merged_layer_patch import MergedLayerPatch, Range
from invokeai.backend.util.original_weights_storage import OriginalWeightsStorage
from tests.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.test_custom_invoke_linear_8_bit_lt import (
build_linear_8bit_lt_layer,
)
from tests.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.test_custom_invoke_linear_nf4 import (
build_linear_nf4_layer,
)
from tests.backend.quantization.gguf.test_ggml_tensor import quantize_tensor
def build_linear_layer_with_ggml_quantized_tensor(orig_layer: torch.nn.Linear | None = None):
if orig_layer is None:
orig_layer = torch.nn.Linear(32, 64)
ggml_quantized_weight = quantize_tensor(orig_layer.weight, gguf.GGMLQuantizationType.Q8_0)
orig_layer.weight = torch.nn.Parameter(ggml_quantized_weight)
ggml_quantized_bias = quantize_tensor(orig_layer.bias, gguf.GGMLQuantizationType.Q8_0)
orig_layer.bias = torch.nn.Parameter(ggml_quantized_bias)
return orig_layer
parameterize_all_devices = pytest.mark.parametrize(
("device"),
[
pytest.param("cpu"),
pytest.param(
"mps", marks=pytest.mark.skipif(not torch.backends.mps.is_available(), reason="MPS is not available.")
),
pytest.param("cuda", marks=pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is not available.")),
],
)
parameterize_cuda_and_mps = pytest.mark.parametrize(
("device"),
[
pytest.param("cuda", marks=pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is not available.")),
pytest.param(
"mps", marks=pytest.mark.skipif(not torch.backends.mps.is_available(), reason="MPS is not available.")
),
],
)
LayerUnderTest = tuple[torch.nn.Module, torch.Tensor, bool]
@pytest.fixture(
params=[
"linear",
"conv1d",
"conv2d",
"group_norm",
"embedding",
"flux_rms_norm",
"linear_with_ggml_quantized_tensor",
"invoke_linear_8_bit_lt",
"invoke_linear_nf4",
]
)
def layer_under_test(request: pytest.FixtureRequest) -> LayerUnderTest:
"""A fixture that returns a tuple of (layer, input, supports_cpu_inference) for the layer under test."""
layer_type = request.param
if layer_type == "linear":
return (torch.nn.Linear(8, 16), torch.randn(1, 8), True)
elif layer_type == "conv1d":
return (torch.nn.Conv1d(8, 16, 3), torch.randn(1, 8, 5), True)
elif layer_type == "conv2d":
return (torch.nn.Conv2d(8, 16, 3), torch.randn(1, 8, 5, 5), True)
elif layer_type == "group_norm":
return (torch.nn.GroupNorm(2, 8), torch.randn(1, 8, 5), True)
elif layer_type == "embedding":
return (torch.nn.Embedding(4, 8), torch.tensor([0, 1], dtype=torch.long), True)
elif layer_type == "flux_rms_norm":
return (RMSNorm(8), torch.randn(1, 8), True)
elif layer_type == "linear_with_ggml_quantized_tensor":
return (build_linear_layer_with_ggml_quantized_tensor(), torch.randn(1, 32), True)
elif layer_type == "invoke_linear_8_bit_lt":
return (build_linear_8bit_lt_layer(), torch.randn(1, 32), False)
elif layer_type == "invoke_linear_nf4":
return (build_linear_nf4_layer(), torch.randn(1, 64), False)
else:
raise ValueError(f"Unsupported layer_type: {layer_type}")
def layer_to_device_via_state_dict(layer: torch.nn.Module, device: str):
"""A helper function to move a layer to a device by roundtripping through a state dict. This most closely matches
how models are moved in the app. Some of the quantization types have broken semantics around calling .to() on the
layer directly, so this is a workaround.
We should fix this in the future.
Relevant article: https://pytorch.org/tutorials/recipes/recipes/swap_tensors.html
"""
state_dict = layer.state_dict()
state_dict = {k: v.to(device) for k, v in state_dict.items()}
layer.load_state_dict(state_dict, assign=True)
def wrap_single_custom_layer(layer: torch.nn.Module):
custom_layer_type = AUTOCAST_MODULE_TYPE_MAPPING[type(layer)]
return wrap_custom_layer(layer, custom_layer_type)
def unwrap_single_custom_layer(layer: torch.nn.Module):
orig_layer_type = AUTOCAST_MODULE_TYPE_MAPPING_INVERSE[type(layer)]
return unwrap_custom_layer(layer, orig_layer_type)
class ZeroParamPatch(BaseLayerPatch):
"""A minimal parameter patch that exercises the aggregated sidecar patch path."""
def get_parameters(self, orig_parameters: dict[str, torch.Tensor], weight: float) -> dict[str, torch.Tensor]:
return {name: torch.zeros_like(param) for name, param in orig_parameters.items()}
def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None):
return self
def calc_size(self) -> int:
return 0
def _cpu_dtype_supported(
layer_factory: Callable[[], torch.nn.Module],
input_factory: Callable[[torch.dtype], torch.Tensor],
dtype: torch.dtype,
) -> bool:
try:
layer = layer_factory().to(dtype=dtype)
input_tensor = input_factory(dtype)
with torch.no_grad():
_ = layer(input_tensor)
return True
except (RuntimeError, TypeError, NotImplementedError):
return False
def _cpu_dtype_param(
dtype: torch.dtype,
layer_factory: Callable[[], torch.nn.Module],
input_factory: Callable[[torch.dtype], torch.Tensor],
):
supported = _cpu_dtype_supported(layer_factory, input_factory, dtype)
return pytest.param(
dtype,
id=str(dtype).removeprefix("torch."),
marks=pytest.mark.skipif(not supported, reason=f"CPU {dtype} is not supported for this op"),
)
LINEAR_CPU_MIXED_DTYPE_PARAMS = [
_cpu_dtype_param(torch.bfloat16, lambda: torch.nn.Linear(8, 16), lambda dtype: torch.randn(2, 8, dtype=dtype)),
_cpu_dtype_param(torch.float16, lambda: torch.nn.Linear(8, 16), lambda dtype: torch.randn(2, 8, dtype=dtype)),
]
CONV2D_CPU_MIXED_DTYPE_PARAMS = [
_cpu_dtype_param(
torch.bfloat16,
lambda: torch.nn.Conv2d(8, 16, 3),
lambda dtype: torch.randn(2, 8, 5, 5, dtype=dtype),
),
_cpu_dtype_param(
torch.float16,
lambda: torch.nn.Conv2d(8, 16, 3),
lambda dtype: torch.randn(2, 8, 5, 5, dtype=dtype),
),
]
def test_isinstance(layer_under_test: LayerUnderTest):
"""Test that isinstance() and type() behave as expected after wrapping a layer in a custom layer."""
orig_layer, _, _ = layer_under_test
orig_type = type(orig_layer)
custom_layer = wrap_single_custom_layer(orig_layer)
assert isinstance(custom_layer, orig_type)
assert type(custom_layer) is not orig_type
def test_wrap_and_unwrap(layer_under_test: LayerUnderTest):
"""Test that wrapping and unwrapping a layer behaves as expected."""
orig_layer, _, _ = layer_under_test
orig_type = type(orig_layer)
# Wrap the original layer and assert that attributes of the custom layer can be accessed.
custom_layer = wrap_single_custom_layer(orig_layer)
custom_layer.set_device_autocasting_enabled(True)
assert custom_layer._device_autocasting_enabled
# Unwrap the custom layer.
# Assert that the methods of the wrapped layer are no longer accessible.
unwrapped_layer = unwrap_single_custom_layer(custom_layer)
with pytest.raises(AttributeError):
_ = unwrapped_layer.set_device_autocasting_enabled(True)
# For now, we have chosen to allow attributes to persist. We may revisit this in the future.
assert unwrapped_layer._device_autocasting_enabled
assert type(unwrapped_layer) is orig_type
@parameterize_all_devices
def test_state_dict(device: str, layer_under_test: LayerUnderTest):
"""Test that .state_dict() behaves the same on the original layer and the wrapped layer."""
orig_layer, _, _ = layer_under_test
# Get the original layer on the test device.
orig_layer.to(device)
orig_state_dict = orig_layer.state_dict()
# Wrap the original layer.
custom_layer = copy.deepcopy(orig_layer)
custom_layer = wrap_single_custom_layer(custom_layer)
custom_state_dict = custom_layer.state_dict()
assert set(orig_state_dict.keys()) == set(custom_state_dict.keys())
for k in orig_state_dict:
assert orig_state_dict[k].shape == custom_state_dict[k].shape
assert orig_state_dict[k].dtype == custom_state_dict[k].dtype
assert orig_state_dict[k].device == custom_state_dict[k].device
assert torch.allclose(orig_state_dict[k], custom_state_dict[k])
@parameterize_all_devices
def test_load_state_dict(device: str, layer_under_test: LayerUnderTest):
"""Test that .load_state_dict() behaves the same on the original layer and the wrapped layer."""
orig_layer, _, _ = layer_under_test
orig_layer.to(device)
custom_layer = copy.deepcopy(orig_layer)
custom_layer = wrap_single_custom_layer(custom_layer)
# Do a state dict roundtrip.
orig_state_dict = orig_layer.state_dict()
custom_state_dict = custom_layer.state_dict()
orig_layer.load_state_dict(custom_state_dict, assign=True)
custom_layer.load_state_dict(orig_state_dict, assign=True)
orig_state_dict = orig_layer.state_dict()
custom_state_dict = custom_layer.state_dict()
# Assert that the state dicts are the same after the roundtrip.
assert set(orig_state_dict.keys()) == set(custom_state_dict.keys())
for k in orig_state_dict:
assert orig_state_dict[k].shape == custom_state_dict[k].shape
assert orig_state_dict[k].dtype == custom_state_dict[k].dtype
assert orig_state_dict[k].device == custom_state_dict[k].device
assert torch.allclose(orig_state_dict[k], custom_state_dict[k])
@parameterize_all_devices
def test_inference_on_device(device: str, layer_under_test: LayerUnderTest):
"""Test that inference behaves the same on the original layer and the wrapped layer when all weights are on the
device.
"""
orig_layer, layer_input, supports_cpu_inference = layer_under_test
if device == "cpu" and not supports_cpu_inference:
pytest.skip("Layer does not support CPU inference.")
layer_to_device_via_state_dict(orig_layer, device)
custom_layer = copy.deepcopy(orig_layer)
custom_layer = wrap_single_custom_layer(custom_layer)
# Run inference with the original layer.
x = layer_input.to(device)
orig_output = orig_layer(x)
# Run inference with the wrapped layer.
custom_output = custom_layer(x)
assert torch.allclose(orig_output, custom_output)
@parameterize_cuda_and_mps
def test_inference_autocast_from_cpu_to_device(device: str, layer_under_test: LayerUnderTest):
"""Test that inference behaves the same on the original layer and the wrapped layer when all weights are on the
device.
"""
orig_layer, layer_input, supports_cpu_inference = layer_under_test
if device == "cpu" and not supports_cpu_inference:
pytest.skip("Layer does not support CPU inference.")
# Make sure the original layer is on the device.
layer_to_device_via_state_dict(orig_layer, device)
x = layer_input.to(device)
# Run inference with the original layer on the device.
orig_output = orig_layer(x)
# Move the original layer to the CPU.
layer_to_device_via_state_dict(orig_layer, "cpu")
is_nf4_layer = type(orig_layer).__name__ == "InvokeLinearNF4"
# Inference should fail with an input on the device. Do not probe raw NF4 here: with CPU-stored weights and a
# single-row CUDA input, some bitsandbytes versions hit an unsafe gemv_4bit path instead of raising safely.
if not is_nf4_layer:
with pytest.raises((RuntimeError, ValueError)):
_ = orig_layer(x)
# Wrap the original layer.
custom_layer = copy.deepcopy(orig_layer)
custom_layer = wrap_single_custom_layer(custom_layer)
# Inference should still fail with autocasting disabled. See the raw NF4 note above.
custom_layer.set_device_autocasting_enabled(False)
if not is_nf4_layer:
with pytest.raises((RuntimeError, ValueError)):
_ = custom_layer(x)
# Run inference with the wrapped layer on the device.
custom_layer.set_device_autocasting_enabled(True)
custom_output = custom_layer(x)
assert custom_output.device.type == device
if is_nf4_layer:
assert torch.allclose(orig_output, custom_output, atol=1e-5)
else:
assert torch.allclose(orig_output, custom_output)
PatchUnderTest = tuple[list[tuple[BaseLayerPatch, float]], torch.Tensor]
def _has_dora_patch(patches: list[tuple[BaseLayerPatch, float]]) -> bool:
return any(isinstance(patch, DoRALayer) for patch, _ in patches)
def _is_bnb_quantized_linear(layer: torch.nn.Module) -> bool:
return type(layer).__name__ in {"InvokeLinear8bitLt", "InvokeLinearNF4"}
@pytest.fixture(
params=[
"single_lora",
"multiple_loras",
"concatenated_lora",
"flux_control_lora",
"single_lokr",
"single_dora",
]
)
def patch_under_test(request: pytest.FixtureRequest) -> PatchUnderTest:
"""A fixture that returns a tuple of (patches, input) for the patch under test."""
layer_type = request.param
torch.manual_seed(0)
# The assumed in/out features of the base linear layer.
in_features = 32
out_features = 64
rank = 4
if layer_type == "single_lora":
lora_layer = LoRALayer(
up=torch.randn(out_features, rank),
mid=None,
down=torch.randn(rank, in_features),
alpha=1.0,
bias=torch.randn(out_features),
)
input = torch.randn(1, in_features)
return ([(lora_layer, 0.7)], input)
elif layer_type == "multiple_loras":
lora_layer = LoRALayer(
up=torch.randn(out_features, rank),
mid=None,
down=torch.randn(rank, in_features),
alpha=1.0,
bias=torch.randn(out_features),
)
lora_layer_2 = LoRALayer(
up=torch.randn(out_features, rank),
mid=None,
down=torch.randn(rank, in_features),
alpha=1.0,
bias=torch.randn(out_features),
)
input = torch.randn(1, in_features)
return ([(lora_layer, 1.0), (lora_layer_2, 0.5)], input)
elif layer_type == "concatenated_lora":
sub_layer_out_features = [16, 16, 32]
# Create a MergedLayerPatch.
sub_layers: list[LoRALayer] = []
sub_layer_ranges: list[Range] = []
dim_0_offset = 0
for out_features in sub_layer_out_features:
down = torch.randn(rank, in_features)
up = torch.randn(out_features, rank)
bias = torch.randn(out_features)
sub_layers.append(LoRALayer(up=up, mid=None, down=down, alpha=1.0, bias=bias))
sub_layer_ranges.append(Range(dim_0_offset, dim_0_offset + out_features))
dim_0_offset += out_features
merged_layer_patch = MergedLayerPatch(sub_layers, sub_layer_ranges)
input = torch.randn(1, in_features)
return ([(merged_layer_patch, 0.7)], input)
elif layer_type == "flux_control_lora":
# Create a FluxControlLoRALayer.
patched_in_features = 40
lora_layer = FluxControlLoRALayer(
up=torch.randn(out_features, rank),
mid=None,
down=torch.randn(rank, patched_in_features),
alpha=1.0,
bias=torch.randn(out_features),
)
input = torch.randn(1, patched_in_features)
return ([(lora_layer, 0.7)], input)
elif layer_type == "single_lokr":
lokr_layer = LoKRLayer(
w1=torch.randn(rank, rank),
w1_a=None,
w1_b=None,
w2=torch.randn(out_features // rank, in_features // rank),
w2_a=None,
w2_b=None,
t2=None,
alpha=1.0,
bias=torch.randn(out_features),
)
input = torch.randn(1, in_features)
return ([(lokr_layer, 0.7)], input)
elif layer_type == "single_dora":
# Regression coverage for #8624: DoRA + partial-loading + CPU->device autocast.
# Scaled down so the patched weight stays well-conditioned for allclose comparisons.
# dora_scale has shape (1, in_features) to broadcast against direction_norm in
# DoRALayer.get_weight — see dora_layer.py:74-82.
dora_layer = DoRALayer(
up=torch.randn(out_features, rank) * 0.01,
down=torch.randn(rank, in_features) * 0.01,
dora_scale=torch.ones(1, in_features),
alpha=1.0,
bias=torch.randn(out_features) * 0.01,
)
input = torch.randn(1, in_features)
return ([(dora_layer, 0.7)], input)
else:
raise ValueError(f"Unsupported layer_type: {layer_type}")
@parameterize_all_devices
def test_linear_sidecar_patches(device: str, patch_under_test: PatchUnderTest):
patches, input = patch_under_test
torch.manual_seed(0)
# Build the base layer under test.
layer = torch.nn.Linear(32, 64)
# Move the layer and input to the device.
layer_to_device_via_state_dict(layer, device)
input = input.to(torch.device(device))
# Patch the LoRA layer into the linear layer.
layer_patched = copy.deepcopy(layer)
for patch, weight in patches:
LayerPatcher._apply_model_layer_patch(
module_to_patch=layer_patched,
module_to_patch_key="",
patch=patch,
patch_weight=weight,
original_weights=OriginalWeightsStorage(),
)
# Wrap the original layer in a custom layer and add the patch to it as a sidecar.
custom_layer = wrap_single_custom_layer(layer)
for patch, weight in patches:
patch.to(torch.device(device))
custom_layer.add_patch(patch, weight)
# Run inference with the original layer and the patched layer and assert they are equal.
output_patched = layer_patched(input)
output_custom = custom_layer(input)
assert torch.allclose(output_patched, output_custom, atol=1e-6)
@parameterize_cuda_and_mps
def test_linear_sidecar_patches_with_autocast_from_cpu_to_device(device: str, patch_under_test: PatchUnderTest):
"""Test that the output of a linear layer with sidecar patches is the same when the layer is on the device and
when the layer is on the CPU and the patches are autocasted to the device.
"""
patches, input = patch_under_test
# Build the base layer under test.
layer = torch.nn.Linear(32, 64)
# Move the layer and input to the device.
layer_to_device_via_state_dict(layer, device)
input = input.to(torch.device(device))
# Wrap the original layer in a custom layer and add the patch to it.
custom_layer = wrap_single_custom_layer(layer)
for patch, weight in patches:
patch.to(torch.device(device))
custom_layer.add_patch(patch, weight)
# Run inference with the custom layer on the device.
expected_output = custom_layer(input)
# Move the custom layer to the CPU.
layer_to_device_via_state_dict(custom_layer, "cpu")
# Move the patches to the CPU.
custom_layer.clear_patches()
for patch, weight in patches:
patch.to(torch.device("cpu"))
custom_layer.add_patch(patch, weight)
# Run inference with an input on the device, and all layer weights on the CPU. The weights should be autocasted to
# the device.
autocast_output = custom_layer(input)
assert autocast_output.device.type == device
# Assert that the outputs with and without autocasting are the same.
assert torch.allclose(expected_output, autocast_output, atol=1e-6)
@pytest.fixture(
params=[
"linear_ggml_quantized",
"invoke_linear_8_bit_lt",
"invoke_linear_nf4",
]
)
def quantized_linear_layer_under_test(request: pytest.FixtureRequest):
in_features = 32
out_features = 64
torch.manual_seed(0)
layer_type = request.param
orig_layer = torch.nn.Linear(in_features, out_features)
if layer_type == "linear_ggml_quantized":
return orig_layer, build_linear_layer_with_ggml_quantized_tensor(orig_layer)
elif layer_type == "invoke_linear_8_bit_lt":
return orig_layer, build_linear_8bit_lt_layer(orig_layer)
elif layer_type == "invoke_linear_nf4":
return orig_layer, build_linear_nf4_layer(orig_layer)
else:
raise ValueError(f"Unsupported layer_type: {layer_type}")
@parameterize_cuda_and_mps
def test_quantized_linear_sidecar_patches(
device: str,
quantized_linear_layer_under_test: tuple[torch.nn.Module, torch.nn.Module],
patch_under_test: PatchUnderTest,
):
"""Test that patches can be applied to quantized linear layers and that the output is the same as when the patch is
applied to a non-quantized linear layer.
"""
patches, input = patch_under_test
linear_layer, quantized_linear_layer = quantized_linear_layer_under_test
expect_dora_incompatible = _is_bnb_quantized_linear(quantized_linear_layer) and _has_dora_patch(patches)
# Move everything to the device.
layer_to_device_via_state_dict(linear_layer, device)
layer_to_device_via_state_dict(quantized_linear_layer, device)
input = input.to(torch.device(device))
# Wrap both layers in custom layers.
linear_layer_custom = wrap_single_custom_layer(linear_layer)
quantized_linear_layer_custom = wrap_single_custom_layer(quantized_linear_layer)
# Apply the patches to the custom layers.
for patch, weight in patches:
patch.to(torch.device(device))
linear_layer_custom.add_patch(patch, weight)
quantized_linear_layer_custom.add_patch(patch, weight)
# Run inference with the original layer and the patched layer and assert they are equal.
output_linear_patched = linear_layer_custom(input)
if expect_dora_incompatible:
with pytest.raises(RuntimeError, match="not compatible with DoRA patches"):
quantized_linear_layer_custom(input)
return
output_quantized_patched = quantized_linear_layer_custom(input)
assert torch.allclose(output_linear_patched, output_quantized_patched, rtol=0.2, atol=0.2)
@parameterize_cuda_and_mps
def test_quantized_linear_sidecar_patches_with_autocast_from_cpu_to_device(
device: str,
quantized_linear_layer_under_test: tuple[torch.nn.Module, torch.nn.Module],
patch_under_test: PatchUnderTest,
):
"""Test that the output of a linear layer with sidecar patches is the same when the layer is on the device and
when the layer is on the CPU and the patches are autocasted to the device.
"""
patches, input = patch_under_test
_, quantized_linear_layer = quantized_linear_layer_under_test
expect_dora_incompatible = _is_bnb_quantized_linear(quantized_linear_layer) and _has_dora_patch(patches)
# Move everything to the device.
layer_to_device_via_state_dict(quantized_linear_layer, device)
input = input.to(torch.device(device))
# Wrap the quantized linear layer in a custom layer and add the patch to it.
quantized_linear_layer_custom = wrap_single_custom_layer(quantized_linear_layer)
for patch, weight in patches:
patch.to(torch.device(device))
quantized_linear_layer_custom.add_patch(patch, weight)
# Run inference with the custom layer on the device.
if expect_dora_incompatible:
with pytest.raises(RuntimeError, match="not compatible with DoRA patches"):
quantized_linear_layer_custom(input)
return
expected_output = quantized_linear_layer_custom(input)
# Move the custom layer to the CPU.
layer_to_device_via_state_dict(quantized_linear_layer_custom, "cpu")
# Move the patches to the CPU.
quantized_linear_layer_custom.clear_patches()
for patch, weight in patches:
patch.to(torch.device("cpu"))
quantized_linear_layer_custom.add_patch(patch, weight)
# Run inference with an input on the device, and all layer weights on the CPU. The weights should be autocasted to
# the device.
autocast_output = quantized_linear_layer_custom(input)
assert autocast_output.device.type == device
# Assert that the outputs with and without autocasting are the same.
assert torch.allclose(expected_output, autocast_output, atol=1e-6)
@pytest.mark.parametrize("dtype", LINEAR_CPU_MIXED_DTYPE_PARAMS)
@torch.no_grad()
def test_linear_mixed_dtype_inference_without_patches(dtype: torch.dtype):
layer = wrap_single_custom_layer(torch.nn.Linear(8, 16))
input = torch.randn(2, 8, dtype=dtype)
output = layer(input)
assert output.dtype == input.dtype
assert output.shape == (2, 16)
@pytest.mark.parametrize("dtype", LINEAR_CPU_MIXED_DTYPE_PARAMS)
@torch.no_grad()
def test_linear_mixed_dtype_inference_without_patches_bias_only_mismatch(dtype: torch.dtype):
layer = torch.nn.Linear(8, 16).to(dtype=dtype)
layer.bias = torch.nn.Parameter(layer.bias.detach().to(torch.float32))
layer = wrap_single_custom_layer(layer)
input = torch.randn(2, 8, dtype=dtype)
output = layer(input)
assert output.dtype == input.dtype
assert output.shape == (2, 16)
@pytest.mark.parametrize("dtype", CONV2D_CPU_MIXED_DTYPE_PARAMS)
@torch.no_grad()
def test_conv2d_mixed_dtype_inference_without_patches(dtype: torch.dtype):
layer = wrap_single_custom_layer(torch.nn.Conv2d(8, 16, 3))
input = torch.randn(2, 8, 5, 5, dtype=dtype)
output = layer(input)
assert output.dtype == input.dtype
assert output.shape == (2, 16, 3, 3)
@pytest.mark.parametrize("dtype", LINEAR_CPU_MIXED_DTYPE_PARAMS)
@torch.no_grad()
def test_linear_mixed_dtype_sidecar_parameter_patch(dtype: torch.dtype):
layer = wrap_single_custom_layer(torch.nn.Linear(8, 16))
layer.add_patch(ZeroParamPatch(), 1.0)
input = torch.randn(2, 8, dtype=dtype)
output = layer(input)
assert output.dtype == input.dtype
assert output.shape == (2, 16)
@pytest.mark.parametrize("dtype", CONV2D_CPU_MIXED_DTYPE_PARAMS)
@torch.no_grad()
def test_conv2d_mixed_dtype_sidecar_parameter_patch(dtype: torch.dtype):
layer = wrap_single_custom_layer(torch.nn.Conv2d(8, 16, 3))
layer.add_patch(ZeroParamPatch(), 1.0)
input = torch.randn(2, 8, 5, 5, dtype=dtype)
output = layer(input)
assert output.dtype == input.dtype
assert output.shape == (2, 16, 3, 3)
@torch.no_grad()
def test_aggregate_patch_parameters_preserves_plain_tensor_with_dora():
"""Regression test for #8624: when partial-loading autocasts a CPU Parameter onto the
compute device, cast_to_device returns a plain torch.Tensor (not a Parameter). The
aggregator must treat that as a real tensor and not substitute a meta-device dummy —
otherwise DoRA's quantization guard falsely triggers on non-quantized base models.
This test is CPU-only and simulates the hand-off by constructing a plain torch.Tensor
directly; the equivalent CUDA/MPS E2E flow is exercised by the "single_dora" variant
of test_linear_sidecar_patches_with_autocast_from_cpu_to_device.
"""
layer = wrap_single_custom_layer(torch.nn.Linear(32, 64))
rank = 4
dora_patch = DoRALayer(
up=torch.randn(64, rank) * 0.01,
down=torch.randn(rank, 32) * 0.01,
dora_scale=torch.ones(1, 32),
alpha=1.0,
bias=None,
)
# Plain torch.Tensor — the shape _cast_weight_bias_for_input hands into
# _aggregate_patch_parameters after autocasting a Parameter across devices.
plain_weight = torch.randn(64, 32)
assert type(plain_weight) is torch.Tensor
orig_params = {"weight": plain_weight}
params = layer._aggregate_patch_parameters(
patches_and_weights=[(dora_patch, 1.0)],
orig_params=orig_params,
device=torch.device("cpu"),
)
# Pre-fix, orig_params["weight"] would have been replaced by a meta-device dummy,
# causing DoRALayer.get_parameters to raise "not compatible with DoRA patches".
assert orig_params["weight"].device.type == "cpu"
assert params["weight"].shape == (64, 32)
assert params["weight"].device.type == "cpu"
assert not torch.isnan(params["weight"]).any()
@@ -0,0 +1,31 @@
import torch
from invokeai.backend.flux.modules.layers import RMSNorm
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_flux_rms_norm import (
CustomFluxRMSNorm,
)
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import (
wrap_custom_layer,
)
from invokeai.backend.patches.layers.set_parameter_layer import SetParameterLayer
def test_custom_flux_rms_norm_patch():
"""Test a SetParameterLayer patch on a CustomFluxRMSNorm layer."""
# Create a RMSNorm layer.
dim = 8
rms_norm = RMSNorm(dim)
# Create a SetParameterLayer.
new_scale = torch.randn(dim)
set_parameter_layer = SetParameterLayer("scale", new_scale)
# Wrap the RMSNorm layer in a CustomFluxRMSNorm layer.
custom_flux_rms_norm = wrap_custom_layer(rms_norm, CustomFluxRMSNorm)
custom_flux_rms_norm.add_patch(set_parameter_layer, 1.0)
# Run the CustomFluxRMSNorm layer.
input = torch.randn(1, dim)
expected_output = torch.nn.functional.rms_norm(input, new_scale.shape, new_scale, eps=1e-6)
output_custom = custom_flux_rms_norm(input)
assert torch.allclose(output_custom, expected_output, atol=1e-6)
@@ -0,0 +1,82 @@
import pytest
import torch
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import (
wrap_custom_layer,
)
if not torch.cuda.is_available():
pytest.skip("CUDA is not available", allow_module_level=True)
else:
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_invoke_linear_8_bit_lt import (
CustomInvokeLinear8bitLt,
)
from invokeai.backend.quantization.bnb_llm_int8 import InvokeLinear8bitLt
def build_linear_8bit_lt_layer(orig_layer: torch.nn.Linear | None = None):
if not torch.cuda.is_available():
pytest.skip("CUDA is not available")
torch.manual_seed(1)
if orig_layer is None:
orig_layer = torch.nn.Linear(32, 64)
orig_layer_state_dict = orig_layer.state_dict()
# Prepare a quantized InvokeLinear8bitLt layer.
quantized_layer = InvokeLinear8bitLt(
input_features=orig_layer.in_features, output_features=orig_layer.out_features, has_fp16_weights=False
)
quantized_layer.load_state_dict(orig_layer_state_dict)
quantized_layer.to("cuda")
# Assert that the InvokeLinear8bitLt layer is quantized.
assert quantized_layer.weight.CB is not None
assert quantized_layer.weight.SCB is not None
assert quantized_layer.weight.CB.dtype == torch.int8
return quantized_layer
@pytest.fixture
def linear_8bit_lt_layer():
return build_linear_8bit_lt_layer()
def test_custom_invoke_linear_8bit_lt_all_weights_on_cuda(linear_8bit_lt_layer: InvokeLinear8bitLt):
"""Test CustomInvokeLinear8bitLt inference with all weights on the GPU."""
# Run inference on the original layer.
x = torch.randn(1, 32).to("cuda")
y_quantized = linear_8bit_lt_layer(x)
# Wrap the InvokeLinear8bitLt layer in a CustomInvokeLinear8bitLt layer, and run inference on it.
custom_linear_8bit_lt_layer = wrap_custom_layer(linear_8bit_lt_layer, CustomInvokeLinear8bitLt)
y_custom = custom_linear_8bit_lt_layer(x)
# Assert that the quantized and custom layers produce the same output.
assert torch.allclose(y_quantized, y_custom, atol=1e-5)
def test_custom_invoke_linear_8bit_lt_all_weights_on_cpu(linear_8bit_lt_layer: InvokeLinear8bitLt):
"""Test CustomInvokeLinear8bitLt inference with all weights on the CPU (streaming to the GPU)."""
# Run inference on the original layer.
x = torch.randn(1, 32).to("cuda")
y_quantized = linear_8bit_lt_layer(x)
# Copy the state dict to the CPU and reload it.
state_dict = linear_8bit_lt_layer.state_dict()
state_dict = {k: v.to("cpu") for k, v in state_dict.items()}
linear_8bit_lt_layer.load_state_dict(state_dict)
# Inference of the original layer should fail.
with pytest.raises((RuntimeError, ValueError)):
linear_8bit_lt_layer(x)
# Wrap the InvokeLinear8bitLt layer in a CustomInvokeLinear8bitLt layer, and run inference on it.
custom_linear_8bit_lt_layer = wrap_custom_layer(linear_8bit_lt_layer, CustomInvokeLinear8bitLt)
custom_linear_8bit_lt_layer.set_device_autocasting_enabled(True)
y_custom = custom_linear_8bit_lt_layer(x)
# Assert that the quantized and custom layers produce the same output.
assert torch.allclose(y_quantized, y_custom, atol=1e-5)
@@ -0,0 +1,108 @@
from unittest.mock import patch
import pytest
import torch
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import (
wrap_custom_layer,
)
if not torch.cuda.is_available():
pytest.skip("CUDA is not available", allow_module_level=True)
else:
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_invoke_linear_nf4 import (
CustomInvokeLinearNF4,
)
from invokeai.backend.quantization.bnb_nf4 import InvokeLinearNF4
def build_linear_nf4_layer(orig_layer: torch.nn.Linear | None = None):
if not torch.cuda.is_available():
pytest.skip("CUDA is not available")
torch.manual_seed(1)
if orig_layer is None:
orig_layer = torch.nn.Linear(64, 16)
orig_layer_state_dict = orig_layer.state_dict()
# Prepare a quantized InvokeLinearNF4 layer.
quantized_layer = InvokeLinearNF4(input_features=orig_layer.in_features, output_features=orig_layer.out_features)
quantized_layer.load_state_dict(orig_layer_state_dict)
quantized_layer.to("cuda")
# Assert that the InvokeLinearNF4 layer is quantized.
assert quantized_layer.weight.bnb_quantized
return quantized_layer
@pytest.fixture
def linear_nf4_layer():
return build_linear_nf4_layer()
def test_custom_invoke_linear_nf4_all_weights_on_cuda(linear_nf4_layer: InvokeLinearNF4):
"""Test CustomInvokeLinearNF4 inference with all weights on the GPU."""
# Run inference on the original layer.
x = torch.randn(1, 64).to("cuda")
y_quantized = linear_nf4_layer(x)
# Wrap the InvokeLinearNF4 layer in a CustomInvokeLinearNF4 layer, and run inference on it.
custom_linear_nf4_layer = wrap_custom_layer(linear_nf4_layer, CustomInvokeLinearNF4)
custom_linear_nf4_layer.set_device_autocasting_enabled(True)
y_custom = custom_linear_nf4_layer(x)
# Assert that the quantized and custom layers produce the same output.
assert torch.allclose(y_quantized, y_custom, atol=1e-5)
def test_custom_invoke_linear_nf4_all_weights_on_cuda_uses_bnb_single_vector_path(
linear_nf4_layer: InvokeLinearNF4,
):
"""GPU-resident single-vector inference should keep using bnb's gemv_4bit path."""
x = torch.randn(1, 64).to("cuda")
custom_linear_nf4_layer = wrap_custom_layer(linear_nf4_layer, CustomInvokeLinearNF4)
custom_linear_nf4_layer.set_device_autocasting_enabled(True)
with patch("bitsandbytes.functional.dequantize_4bit") as mock_dequantize:
_ = custom_linear_nf4_layer(x)
mock_dequantize.assert_not_called()
# We run with two different input dimensions, because the NF4 layer follows a different code path depending on the
# input dimension, and this has caused issues in the past.
@pytest.mark.parametrize("input_dim_0", [1, 2])
def test_custom_invoke_linear_nf4_all_weights_on_cpu(linear_nf4_layer: InvokeLinearNF4, input_dim_0: int):
"""Test CustomInvokeLinearNF4 inference with all weights on the CPU (streaming to the GPU)."""
# Run inference on the original layer.
x = torch.randn(input_dim_0, 64).to(device="cuda")
y_quantized = linear_nf4_layer(x)
# Copy the state dict to the CPU and reload it.
state_dict = linear_nf4_layer.state_dict()
state_dict = {k: v.to("cpu") for k, v in state_dict.items()}
linear_nf4_layer.load_state_dict(state_dict)
# Do not call the raw bitsandbytes NF4 layer here. With CPU-stored weights and a single-row CUDA input, some
# bitsandbytes versions hit an unsafe gemv_4bit path instead of raising a Python exception. The custom layer below
# is the behavior under test.
# Wrap the InvokeLinearNF4 layer in a CustomInvokeLinearNF4 layer, and run inference on it.
custom_linear_nf4_layer = wrap_custom_layer(linear_nf4_layer, CustomInvokeLinearNF4)
custom_linear_nf4_layer.set_device_autocasting_enabled(True)
y_custom = custom_linear_nf4_layer(x)
# Assert that the state dict (and the tensors that it references) are still on the CPU.
assert all(v.device == torch.device("cpu") for v in state_dict.values())
# Assert that the weight, bias, and quant_state are all on the CPU.
assert custom_linear_nf4_layer.weight.device == torch.device("cpu")
assert custom_linear_nf4_layer.bias.device == torch.device("cpu")
assert custom_linear_nf4_layer.weight.quant_state.absmax.device == torch.device("cpu")
assert custom_linear_nf4_layer.weight.quant_state.code.device == torch.device("cpu")
# Assert that the quantized and custom layers produce the same output.
assert torch.allclose(y_quantized, y_custom, atol=1e-5)
@@ -0,0 +1,132 @@
import os
import gguf
import pytest
import torch
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import (
apply_custom_layers_to_model,
remove_custom_layers_from_model,
)
from tests.backend.quantization.gguf.test_ggml_tensor import quantize_tensor
try:
from invokeai.backend.quantization.bnb_llm_int8 import InvokeLinear8bitLt, quantize_model_llm_int8
except ImportError:
# This is expected to fail on MacOS
pass
cuda_and_mps = pytest.mark.parametrize(
"device",
[
pytest.param(
torch.device("cuda"), marks=pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA device")
),
pytest.param(
torch.device("mps"),
marks=pytest.mark.skipif(not torch.backends.mps.is_available(), reason="requires MPS device"),
),
],
)
class ModelWithLinearLayer(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear = torch.nn.Linear(32, 64)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.linear(x)
@pytest.fixture(params=["none", "gguf"])
def model(request: pytest.FixtureRequest) -> torch.nn.Module:
if request.param == "none":
return ModelWithLinearLayer()
elif request.param == "gguf":
# Initialize ModelWithLinearLayer and replace the linear layer weight with a GGML quantized weight.
model = ModelWithLinearLayer()
ggml_quantized_weight = quantize_tensor(model.linear.weight, gguf.GGMLQuantizationType.Q8_0)
model.linear.weight = torch.nn.Parameter(ggml_quantized_weight)
return model
else:
raise ValueError(f"Invalid quantization type: {request.param}")
@cuda_and_mps
@torch.no_grad()
def test_torch_module_autocast_linear_layer(device: torch.device, model: torch.nn.Module):
# Skip this test with MPS on GitHub Actions. It fails but I haven't taken the tie to figure out why. It passes
# locally on MacOS.
if os.environ.get("GITHUB_ACTIONS") == "true" and device.type == "mps":
pytest.skip("This test is flaky on GitHub Actions")
# Model parameters should start off on the CPU.
assert all(p.device.type == "cpu" for p in model.parameters())
torch.manual_seed(0)
# Run inference on the CPU.
x = torch.randn(1, 32, device="cpu")
expected = model(x)
assert expected.device.type == "cpu"
# Apply the custom layers to the model.
apply_custom_layers_to_model(model, device_autocasting_enabled=True)
# Run the model on the device.
autocast_result = model(x.to(device))
# The model output should be on the device.
assert autocast_result.device.type == device.type
# The model parameters should still be on the CPU.
assert all(p.device.type == "cpu" for p in model.parameters())
# Remove the custom layers from the model.
remove_custom_layers_from_model(model)
# After removing the custom layers, the model should no longer be able to run inference on the device.
with pytest.raises(RuntimeError):
_ = model(x.to(device))
# Run inference again on the CPU.
after_result = model(x)
assert after_result.device.type == "cpu"
# The results from all inference runs should be the same.
assert torch.allclose(autocast_result.to("cpu"), expected, atol=1e-5)
assert torch.allclose(after_result, expected, atol=1e-5)
@torch.no_grad()
def test_torch_module_autocast_bnb_llm_int8_linear_layer():
if not torch.cuda.is_available():
pytest.skip("requires CUDA device")
torch.manual_seed(0)
model = ModelWithLinearLayer()
model = quantize_model_llm_int8(model, modules_to_not_convert=set())
# The act of moving the model to the CUDA device will trigger quantization.
model.to("cuda")
# Confirm that the layer is quantized.
assert isinstance(model.linear, InvokeLinear8bitLt)
assert model.linear.weight.CB is not None
assert model.linear.weight.SCB is not None
# Run inference on the GPU.
x = torch.randn(1, 32)
expected = model(x.to("cuda"))
assert expected.device.type == "cuda"
# Move the model back to the CPU and add the custom layers to the model.
model.to("cpu")
apply_custom_layers_to_model(model, device_autocasting_enabled=True)
# Run inference with weights being streamed to the GPU.
autocast_result = model(x.to("cuda"))
assert autocast_result.device.type == "cuda"
# The results from all inference runs should be the same.
assert torch.allclose(autocast_result, expected, atol=1e-5)
@@ -0,0 +1,155 @@
"""Representative key layout of an official Anima transformer single-file checkpoint.
Captured from `anima-base-v1.0.safetensors`. The full checkpoint has 685 tensors under the
`net.` prefix; this fixture keeps `net.blocks.0.*` plus all non-block `net.*` keys. Used to
exercise `_strip_anima_bundle_prefix` (the `net.` -> unprefixed strip). The ComfyUI-bundled
`model.diffusion_model.*` variant is covered by a small synthetic dict in the test.
"""
state_dict_keys: dict[str, list[int]] = {
"net.blocks.0.adaln_modulation_cross_attn.1.weight": [256, 2048],
"net.blocks.0.adaln_modulation_cross_attn.2.weight": [6144, 256],
"net.blocks.0.adaln_modulation_mlp.1.weight": [256, 2048],
"net.blocks.0.adaln_modulation_mlp.2.weight": [6144, 256],
"net.blocks.0.adaln_modulation_self_attn.1.weight": [256, 2048],
"net.blocks.0.adaln_modulation_self_attn.2.weight": [6144, 256],
"net.blocks.0.cross_attn.k_norm.weight": [128],
"net.blocks.0.cross_attn.k_proj.weight": [2048, 1024],
"net.blocks.0.cross_attn.output_proj.weight": [2048, 2048],
"net.blocks.0.cross_attn.q_norm.weight": [128],
"net.blocks.0.cross_attn.q_proj.weight": [2048, 2048],
"net.blocks.0.cross_attn.v_proj.weight": [2048, 1024],
"net.blocks.0.mlp.layer1.weight": [8192, 2048],
"net.blocks.0.mlp.layer2.weight": [2048, 8192],
"net.blocks.0.self_attn.k_norm.weight": [128],
"net.blocks.0.self_attn.k_proj.weight": [2048, 2048],
"net.blocks.0.self_attn.output_proj.weight": [2048, 2048],
"net.blocks.0.self_attn.q_norm.weight": [128],
"net.blocks.0.self_attn.q_proj.weight": [2048, 2048],
"net.blocks.0.self_attn.v_proj.weight": [2048, 2048],
"net.final_layer.adaln_modulation.1.weight": [256, 2048],
"net.final_layer.adaln_modulation.2.weight": [4096, 256],
"net.final_layer.linear.weight": [64, 2048],
"net.llm_adapter.blocks.0.cross_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.0.cross_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.0.cross_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.0.cross_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.0.cross_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.0.cross_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.0.mlp.0.bias": [4096],
"net.llm_adapter.blocks.0.mlp.0.weight": [4096, 1024],
"net.llm_adapter.blocks.0.mlp.2.bias": [1024],
"net.llm_adapter.blocks.0.mlp.2.weight": [1024, 4096],
"net.llm_adapter.blocks.0.norm_cross_attn.weight": [1024],
"net.llm_adapter.blocks.0.norm_mlp.weight": [1024],
"net.llm_adapter.blocks.0.norm_self_attn.weight": [1024],
"net.llm_adapter.blocks.0.self_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.0.self_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.0.self_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.0.self_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.0.self_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.0.self_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.1.cross_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.1.cross_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.1.cross_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.1.cross_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.1.cross_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.1.cross_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.1.mlp.0.bias": [4096],
"net.llm_adapter.blocks.1.mlp.0.weight": [4096, 1024],
"net.llm_adapter.blocks.1.mlp.2.bias": [1024],
"net.llm_adapter.blocks.1.mlp.2.weight": [1024, 4096],
"net.llm_adapter.blocks.1.norm_cross_attn.weight": [1024],
"net.llm_adapter.blocks.1.norm_mlp.weight": [1024],
"net.llm_adapter.blocks.1.norm_self_attn.weight": [1024],
"net.llm_adapter.blocks.1.self_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.1.self_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.1.self_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.1.self_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.1.self_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.1.self_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.2.cross_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.2.cross_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.2.cross_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.2.cross_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.2.cross_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.2.cross_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.2.mlp.0.bias": [4096],
"net.llm_adapter.blocks.2.mlp.0.weight": [4096, 1024],
"net.llm_adapter.blocks.2.mlp.2.bias": [1024],
"net.llm_adapter.blocks.2.mlp.2.weight": [1024, 4096],
"net.llm_adapter.blocks.2.norm_cross_attn.weight": [1024],
"net.llm_adapter.blocks.2.norm_mlp.weight": [1024],
"net.llm_adapter.blocks.2.norm_self_attn.weight": [1024],
"net.llm_adapter.blocks.2.self_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.2.self_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.2.self_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.2.self_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.2.self_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.2.self_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.3.cross_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.3.cross_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.3.cross_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.3.cross_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.3.cross_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.3.cross_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.3.mlp.0.bias": [4096],
"net.llm_adapter.blocks.3.mlp.0.weight": [4096, 1024],
"net.llm_adapter.blocks.3.mlp.2.bias": [1024],
"net.llm_adapter.blocks.3.mlp.2.weight": [1024, 4096],
"net.llm_adapter.blocks.3.norm_cross_attn.weight": [1024],
"net.llm_adapter.blocks.3.norm_mlp.weight": [1024],
"net.llm_adapter.blocks.3.norm_self_attn.weight": [1024],
"net.llm_adapter.blocks.3.self_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.3.self_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.3.self_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.3.self_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.3.self_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.3.self_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.4.cross_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.4.cross_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.4.cross_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.4.cross_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.4.cross_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.4.cross_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.4.mlp.0.bias": [4096],
"net.llm_adapter.blocks.4.mlp.0.weight": [4096, 1024],
"net.llm_adapter.blocks.4.mlp.2.bias": [1024],
"net.llm_adapter.blocks.4.mlp.2.weight": [1024, 4096],
"net.llm_adapter.blocks.4.norm_cross_attn.weight": [1024],
"net.llm_adapter.blocks.4.norm_mlp.weight": [1024],
"net.llm_adapter.blocks.4.norm_self_attn.weight": [1024],
"net.llm_adapter.blocks.4.self_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.4.self_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.4.self_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.4.self_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.4.self_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.4.self_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.5.cross_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.5.cross_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.5.cross_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.5.cross_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.5.cross_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.5.cross_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.5.mlp.0.bias": [4096],
"net.llm_adapter.blocks.5.mlp.0.weight": [4096, 1024],
"net.llm_adapter.blocks.5.mlp.2.bias": [1024],
"net.llm_adapter.blocks.5.mlp.2.weight": [1024, 4096],
"net.llm_adapter.blocks.5.norm_cross_attn.weight": [1024],
"net.llm_adapter.blocks.5.norm_mlp.weight": [1024],
"net.llm_adapter.blocks.5.norm_self_attn.weight": [1024],
"net.llm_adapter.blocks.5.self_attn.k_norm.weight": [64],
"net.llm_adapter.blocks.5.self_attn.k_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.5.self_attn.o_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.5.self_attn.q_norm.weight": [64],
"net.llm_adapter.blocks.5.self_attn.q_proj.weight": [1024, 1024],
"net.llm_adapter.blocks.5.self_attn.v_proj.weight": [1024, 1024],
"net.llm_adapter.embed.weight": [32128, 1024],
"net.llm_adapter.norm.weight": [1024],
"net.llm_adapter.out_proj.bias": [1024],
"net.llm_adapter.out_proj.weight": [1024, 1024],
"net.t_embedder.1.linear_1.weight": [2048, 2048],
"net.t_embedder.1.linear_2.weight": [6144, 2048],
"net.t_embedding_norm.weight": [2048],
"net.x_embedder.proj.1.weight": [2048, 68],
}
@@ -0,0 +1,41 @@
"""Representative BFL-format key layout of a FLUX.2 transformer single-file checkpoint.
Captured from `flux-2-klein-9b-kv.safetensors` (FLUX.2 Klein 9B, bf16). The full
checkpoint has 201 tensors (8 double blocks, 24 single blocks); this
fixture keeps every top-level key plus block 0 of the double/single stacks, which is enough
to exercise `convert_flux2_bfl_to_diffusers` (fused-QKV split, block renames, adaLN
scale/shift swap) and validate against a single-layer `Flux2Transformer2DModel`.
BFL layout uses `double_blocks.*`, `single_blocks.*`, `img_in`, `txt_in`, `time_in`,
`*_modulation.lin`, `final_layer.*`; diffusers expects `transformer_blocks.*`,
`single_transformer_blocks.*`, `x_embedder`, `context_embedder`, `time_guidance_embed.*`,
`proj_out`, `norm_out`.
"""
state_dict_keys: dict[str, list[int]] = {
"double_blocks.0.img_attn.norm.key_norm.scale": [128],
"double_blocks.0.img_attn.norm.query_norm.scale": [128],
"double_blocks.0.img_attn.proj.weight": [4096, 4096],
"double_blocks.0.img_attn.qkv.weight": [12288, 4096],
"double_blocks.0.img_mlp.0.weight": [24576, 4096],
"double_blocks.0.img_mlp.2.weight": [4096, 12288],
"double_blocks.0.txt_attn.norm.key_norm.scale": [128],
"double_blocks.0.txt_attn.norm.query_norm.scale": [128],
"double_blocks.0.txt_attn.proj.weight": [4096, 4096],
"double_blocks.0.txt_attn.qkv.weight": [12288, 4096],
"double_blocks.0.txt_mlp.0.weight": [24576, 4096],
"double_blocks.0.txt_mlp.2.weight": [4096, 12288],
"double_stream_modulation_img.lin.weight": [24576, 4096],
"double_stream_modulation_txt.lin.weight": [24576, 4096],
"final_layer.adaLN_modulation.1.weight": [8192, 4096],
"final_layer.linear.weight": [128, 4096],
"img_in.weight": [4096, 128],
"single_blocks.0.linear1.weight": [36864, 4096],
"single_blocks.0.linear2.weight": [4096, 16384],
"single_blocks.0.norm.key_norm.scale": [128],
"single_blocks.0.norm.query_norm.scale": [128],
"single_stream_modulation.lin.weight": [12288, 4096],
"time_in.in_layer.weight": [4096, 256],
"time_in.out_layer.weight": [4096, 4096],
"txt_in.weight": [4096, 12288],
}
@@ -0,0 +1,266 @@
"""BFL-format key layout of a FLUX.2 VAE single-file checkpoint (full, 251 keys).
Captured from `flux2-vae.safetensors` (standard FLUX.2 VAE, block_out_channels=(128,256,512,512)).
The full key set is kept (the VAE is small), so `convert_flux2_vae_bfl_to_diffusers` can be
validated for *complete* coverage against `AutoencoderKLFlux2` — every converted key must be a
real parameter and every parameter must be covered.
BFL layout uses `encoder.down.*`, `decoder.up.*` (reversed order!), `{enc,dec}.mid.block_N`,
`{enc,dec}.mid.attn_1.*`, `norm_out`, `encoder.quant_conv`, `decoder.post_quant_conv`;
diffusers expects `down_blocks`/`up_blocks`/`mid_block.resnets`/`mid_block.attentions`/
`conv_norm_out` and top-level `quant_conv`/`post_quant_conv`.
"""
state_dict_keys: dict[str, list[int]] = {
"bn.num_batches_tracked": [],
"bn.running_mean": [128],
"bn.running_var": [128],
"decoder.conv_in.bias": [512],
"decoder.conv_in.weight": [512, 32, 3, 3],
"decoder.conv_norm_out.bias": [128],
"decoder.conv_norm_out.weight": [128],
"decoder.conv_out.bias": [3],
"decoder.conv_out.weight": [3, 128, 3, 3],
"decoder.mid_block.attentions.0.group_norm.bias": [512],
"decoder.mid_block.attentions.0.group_norm.weight": [512],
"decoder.mid_block.attentions.0.to_k.bias": [512],
"decoder.mid_block.attentions.0.to_k.weight": [512, 512],
"decoder.mid_block.attentions.0.to_out.0.bias": [512],
"decoder.mid_block.attentions.0.to_out.0.weight": [512, 512],
"decoder.mid_block.attentions.0.to_q.bias": [512],
"decoder.mid_block.attentions.0.to_q.weight": [512, 512],
"decoder.mid_block.attentions.0.to_v.bias": [512],
"decoder.mid_block.attentions.0.to_v.weight": [512, 512],
"decoder.mid_block.resnets.0.conv1.bias": [512],
"decoder.mid_block.resnets.0.conv1.weight": [512, 512, 3, 3],
"decoder.mid_block.resnets.0.conv2.bias": [512],
"decoder.mid_block.resnets.0.conv2.weight": [512, 512, 3, 3],
"decoder.mid_block.resnets.0.norm1.bias": [512],
"decoder.mid_block.resnets.0.norm1.weight": [512],
"decoder.mid_block.resnets.0.norm2.bias": [512],
"decoder.mid_block.resnets.0.norm2.weight": [512],
"decoder.mid_block.resnets.1.conv1.bias": [512],
"decoder.mid_block.resnets.1.conv1.weight": [512, 512, 3, 3],
"decoder.mid_block.resnets.1.conv2.bias": [512],
"decoder.mid_block.resnets.1.conv2.weight": [512, 512, 3, 3],
"decoder.mid_block.resnets.1.norm1.bias": [512],
"decoder.mid_block.resnets.1.norm1.weight": [512],
"decoder.mid_block.resnets.1.norm2.bias": [512],
"decoder.mid_block.resnets.1.norm2.weight": [512],
"decoder.up_blocks.0.resnets.0.conv1.bias": [512],
"decoder.up_blocks.0.resnets.0.conv1.weight": [512, 512, 3, 3],
"decoder.up_blocks.0.resnets.0.conv2.bias": [512],
"decoder.up_blocks.0.resnets.0.conv2.weight": [512, 512, 3, 3],
"decoder.up_blocks.0.resnets.0.norm1.bias": [512],
"decoder.up_blocks.0.resnets.0.norm1.weight": [512],
"decoder.up_blocks.0.resnets.0.norm2.bias": [512],
"decoder.up_blocks.0.resnets.0.norm2.weight": [512],
"decoder.up_blocks.0.resnets.1.conv1.bias": [512],
"decoder.up_blocks.0.resnets.1.conv1.weight": [512, 512, 3, 3],
"decoder.up_blocks.0.resnets.1.conv2.bias": [512],
"decoder.up_blocks.0.resnets.1.conv2.weight": [512, 512, 3, 3],
"decoder.up_blocks.0.resnets.1.norm1.bias": [512],
"decoder.up_blocks.0.resnets.1.norm1.weight": [512],
"decoder.up_blocks.0.resnets.1.norm2.bias": [512],
"decoder.up_blocks.0.resnets.1.norm2.weight": [512],
"decoder.up_blocks.0.resnets.2.conv1.bias": [512],
"decoder.up_blocks.0.resnets.2.conv1.weight": [512, 512, 3, 3],
"decoder.up_blocks.0.resnets.2.conv2.bias": [512],
"decoder.up_blocks.0.resnets.2.conv2.weight": [512, 512, 3, 3],
"decoder.up_blocks.0.resnets.2.norm1.bias": [512],
"decoder.up_blocks.0.resnets.2.norm1.weight": [512],
"decoder.up_blocks.0.resnets.2.norm2.bias": [512],
"decoder.up_blocks.0.resnets.2.norm2.weight": [512],
"decoder.up_blocks.0.upsamplers.0.conv.bias": [512],
"decoder.up_blocks.0.upsamplers.0.conv.weight": [512, 512, 3, 3],
"decoder.up_blocks.1.resnets.0.conv1.bias": [512],
"decoder.up_blocks.1.resnets.0.conv1.weight": [512, 512, 3, 3],
"decoder.up_blocks.1.resnets.0.conv2.bias": [512],
"decoder.up_blocks.1.resnets.0.conv2.weight": [512, 512, 3, 3],
"decoder.up_blocks.1.resnets.0.norm1.bias": [512],
"decoder.up_blocks.1.resnets.0.norm1.weight": [512],
"decoder.up_blocks.1.resnets.0.norm2.bias": [512],
"decoder.up_blocks.1.resnets.0.norm2.weight": [512],
"decoder.up_blocks.1.resnets.1.conv1.bias": [512],
"decoder.up_blocks.1.resnets.1.conv1.weight": [512, 512, 3, 3],
"decoder.up_blocks.1.resnets.1.conv2.bias": [512],
"decoder.up_blocks.1.resnets.1.conv2.weight": [512, 512, 3, 3],
"decoder.up_blocks.1.resnets.1.norm1.bias": [512],
"decoder.up_blocks.1.resnets.1.norm1.weight": [512],
"decoder.up_blocks.1.resnets.1.norm2.bias": [512],
"decoder.up_blocks.1.resnets.1.norm2.weight": [512],
"decoder.up_blocks.1.resnets.2.conv1.bias": [512],
"decoder.up_blocks.1.resnets.2.conv1.weight": [512, 512, 3, 3],
"decoder.up_blocks.1.resnets.2.conv2.bias": [512],
"decoder.up_blocks.1.resnets.2.conv2.weight": [512, 512, 3, 3],
"decoder.up_blocks.1.resnets.2.norm1.bias": [512],
"decoder.up_blocks.1.resnets.2.norm1.weight": [512],
"decoder.up_blocks.1.resnets.2.norm2.bias": [512],
"decoder.up_blocks.1.resnets.2.norm2.weight": [512],
"decoder.up_blocks.1.upsamplers.0.conv.bias": [512],
"decoder.up_blocks.1.upsamplers.0.conv.weight": [512, 512, 3, 3],
"decoder.up_blocks.2.resnets.0.conv1.bias": [256],
"decoder.up_blocks.2.resnets.0.conv1.weight": [256, 512, 3, 3],
"decoder.up_blocks.2.resnets.0.conv2.bias": [256],
"decoder.up_blocks.2.resnets.0.conv2.weight": [256, 256, 3, 3],
"decoder.up_blocks.2.resnets.0.conv_shortcut.bias": [256],
"decoder.up_blocks.2.resnets.0.conv_shortcut.weight": [256, 512, 1, 1],
"decoder.up_blocks.2.resnets.0.norm1.bias": [512],
"decoder.up_blocks.2.resnets.0.norm1.weight": [512],
"decoder.up_blocks.2.resnets.0.norm2.bias": [256],
"decoder.up_blocks.2.resnets.0.norm2.weight": [256],
"decoder.up_blocks.2.resnets.1.conv1.bias": [256],
"decoder.up_blocks.2.resnets.1.conv1.weight": [256, 256, 3, 3],
"decoder.up_blocks.2.resnets.1.conv2.bias": [256],
"decoder.up_blocks.2.resnets.1.conv2.weight": [256, 256, 3, 3],
"decoder.up_blocks.2.resnets.1.norm1.bias": [256],
"decoder.up_blocks.2.resnets.1.norm1.weight": [256],
"decoder.up_blocks.2.resnets.1.norm2.bias": [256],
"decoder.up_blocks.2.resnets.1.norm2.weight": [256],
"decoder.up_blocks.2.resnets.2.conv1.bias": [256],
"decoder.up_blocks.2.resnets.2.conv1.weight": [256, 256, 3, 3],
"decoder.up_blocks.2.resnets.2.conv2.bias": [256],
"decoder.up_blocks.2.resnets.2.conv2.weight": [256, 256, 3, 3],
"decoder.up_blocks.2.resnets.2.norm1.bias": [256],
"decoder.up_blocks.2.resnets.2.norm1.weight": [256],
"decoder.up_blocks.2.resnets.2.norm2.bias": [256],
"decoder.up_blocks.2.resnets.2.norm2.weight": [256],
"decoder.up_blocks.2.upsamplers.0.conv.bias": [256],
"decoder.up_blocks.2.upsamplers.0.conv.weight": [256, 256, 3, 3],
"decoder.up_blocks.3.resnets.0.conv1.bias": [128],
"decoder.up_blocks.3.resnets.0.conv1.weight": [128, 256, 3, 3],
"decoder.up_blocks.3.resnets.0.conv2.bias": [128],
"decoder.up_blocks.3.resnets.0.conv2.weight": [128, 128, 3, 3],
"decoder.up_blocks.3.resnets.0.conv_shortcut.bias": [128],
"decoder.up_blocks.3.resnets.0.conv_shortcut.weight": [128, 256, 1, 1],
"decoder.up_blocks.3.resnets.0.norm1.bias": [256],
"decoder.up_blocks.3.resnets.0.norm1.weight": [256],
"decoder.up_blocks.3.resnets.0.norm2.bias": [128],
"decoder.up_blocks.3.resnets.0.norm2.weight": [128],
"decoder.up_blocks.3.resnets.1.conv1.bias": [128],
"decoder.up_blocks.3.resnets.1.conv1.weight": [128, 128, 3, 3],
"decoder.up_blocks.3.resnets.1.conv2.bias": [128],
"decoder.up_blocks.3.resnets.1.conv2.weight": [128, 128, 3, 3],
"decoder.up_blocks.3.resnets.1.norm1.bias": [128],
"decoder.up_blocks.3.resnets.1.norm1.weight": [128],
"decoder.up_blocks.3.resnets.1.norm2.bias": [128],
"decoder.up_blocks.3.resnets.1.norm2.weight": [128],
"decoder.up_blocks.3.resnets.2.conv1.bias": [128],
"decoder.up_blocks.3.resnets.2.conv1.weight": [128, 128, 3, 3],
"decoder.up_blocks.3.resnets.2.conv2.bias": [128],
"decoder.up_blocks.3.resnets.2.conv2.weight": [128, 128, 3, 3],
"decoder.up_blocks.3.resnets.2.norm1.bias": [128],
"decoder.up_blocks.3.resnets.2.norm1.weight": [128],
"decoder.up_blocks.3.resnets.2.norm2.bias": [128],
"decoder.up_blocks.3.resnets.2.norm2.weight": [128],
"encoder.conv_in.bias": [128],
"encoder.conv_in.weight": [128, 3, 3, 3],
"encoder.conv_norm_out.bias": [512],
"encoder.conv_norm_out.weight": [512],
"encoder.conv_out.bias": [64],
"encoder.conv_out.weight": [64, 512, 3, 3],
"encoder.down_blocks.0.downsamplers.0.conv.bias": [128],
"encoder.down_blocks.0.downsamplers.0.conv.weight": [128, 128, 3, 3],
"encoder.down_blocks.0.resnets.0.conv1.bias": [128],
"encoder.down_blocks.0.resnets.0.conv1.weight": [128, 128, 3, 3],
"encoder.down_blocks.0.resnets.0.conv2.bias": [128],
"encoder.down_blocks.0.resnets.0.conv2.weight": [128, 128, 3, 3],
"encoder.down_blocks.0.resnets.0.norm1.bias": [128],
"encoder.down_blocks.0.resnets.0.norm1.weight": [128],
"encoder.down_blocks.0.resnets.0.norm2.bias": [128],
"encoder.down_blocks.0.resnets.0.norm2.weight": [128],
"encoder.down_blocks.0.resnets.1.conv1.bias": [128],
"encoder.down_blocks.0.resnets.1.conv1.weight": [128, 128, 3, 3],
"encoder.down_blocks.0.resnets.1.conv2.bias": [128],
"encoder.down_blocks.0.resnets.1.conv2.weight": [128, 128, 3, 3],
"encoder.down_blocks.0.resnets.1.norm1.bias": [128],
"encoder.down_blocks.0.resnets.1.norm1.weight": [128],
"encoder.down_blocks.0.resnets.1.norm2.bias": [128],
"encoder.down_blocks.0.resnets.1.norm2.weight": [128],
"encoder.down_blocks.1.downsamplers.0.conv.bias": [256],
"encoder.down_blocks.1.downsamplers.0.conv.weight": [256, 256, 3, 3],
"encoder.down_blocks.1.resnets.0.conv1.bias": [256],
"encoder.down_blocks.1.resnets.0.conv1.weight": [256, 128, 3, 3],
"encoder.down_blocks.1.resnets.0.conv2.bias": [256],
"encoder.down_blocks.1.resnets.0.conv2.weight": [256, 256, 3, 3],
"encoder.down_blocks.1.resnets.0.conv_shortcut.bias": [256],
"encoder.down_blocks.1.resnets.0.conv_shortcut.weight": [256, 128, 1, 1],
"encoder.down_blocks.1.resnets.0.norm1.bias": [128],
"encoder.down_blocks.1.resnets.0.norm1.weight": [128],
"encoder.down_blocks.1.resnets.0.norm2.bias": [256],
"encoder.down_blocks.1.resnets.0.norm2.weight": [256],
"encoder.down_blocks.1.resnets.1.conv1.bias": [256],
"encoder.down_blocks.1.resnets.1.conv1.weight": [256, 256, 3, 3],
"encoder.down_blocks.1.resnets.1.conv2.bias": [256],
"encoder.down_blocks.1.resnets.1.conv2.weight": [256, 256, 3, 3],
"encoder.down_blocks.1.resnets.1.norm1.bias": [256],
"encoder.down_blocks.1.resnets.1.norm1.weight": [256],
"encoder.down_blocks.1.resnets.1.norm2.bias": [256],
"encoder.down_blocks.1.resnets.1.norm2.weight": [256],
"encoder.down_blocks.2.downsamplers.0.conv.bias": [512],
"encoder.down_blocks.2.downsamplers.0.conv.weight": [512, 512, 3, 3],
"encoder.down_blocks.2.resnets.0.conv1.bias": [512],
"encoder.down_blocks.2.resnets.0.conv1.weight": [512, 256, 3, 3],
"encoder.down_blocks.2.resnets.0.conv2.bias": [512],
"encoder.down_blocks.2.resnets.0.conv2.weight": [512, 512, 3, 3],
"encoder.down_blocks.2.resnets.0.conv_shortcut.bias": [512],
"encoder.down_blocks.2.resnets.0.conv_shortcut.weight": [512, 256, 1, 1],
"encoder.down_blocks.2.resnets.0.norm1.bias": [256],
"encoder.down_blocks.2.resnets.0.norm1.weight": [256],
"encoder.down_blocks.2.resnets.0.norm2.bias": [512],
"encoder.down_blocks.2.resnets.0.norm2.weight": [512],
"encoder.down_blocks.2.resnets.1.conv1.bias": [512],
"encoder.down_blocks.2.resnets.1.conv1.weight": [512, 512, 3, 3],
"encoder.down_blocks.2.resnets.1.conv2.bias": [512],
"encoder.down_blocks.2.resnets.1.conv2.weight": [512, 512, 3, 3],
"encoder.down_blocks.2.resnets.1.norm1.bias": [512],
"encoder.down_blocks.2.resnets.1.norm1.weight": [512],
"encoder.down_blocks.2.resnets.1.norm2.bias": [512],
"encoder.down_blocks.2.resnets.1.norm2.weight": [512],
"encoder.down_blocks.3.resnets.0.conv1.bias": [512],
"encoder.down_blocks.3.resnets.0.conv1.weight": [512, 512, 3, 3],
"encoder.down_blocks.3.resnets.0.conv2.bias": [512],
"encoder.down_blocks.3.resnets.0.conv2.weight": [512, 512, 3, 3],
"encoder.down_blocks.3.resnets.0.norm1.bias": [512],
"encoder.down_blocks.3.resnets.0.norm1.weight": [512],
"encoder.down_blocks.3.resnets.0.norm2.bias": [512],
"encoder.down_blocks.3.resnets.0.norm2.weight": [512],
"encoder.down_blocks.3.resnets.1.conv1.bias": [512],
"encoder.down_blocks.3.resnets.1.conv1.weight": [512, 512, 3, 3],
"encoder.down_blocks.3.resnets.1.conv2.bias": [512],
"encoder.down_blocks.3.resnets.1.conv2.weight": [512, 512, 3, 3],
"encoder.down_blocks.3.resnets.1.norm1.bias": [512],
"encoder.down_blocks.3.resnets.1.norm1.weight": [512],
"encoder.down_blocks.3.resnets.1.norm2.bias": [512],
"encoder.down_blocks.3.resnets.1.norm2.weight": [512],
"encoder.mid_block.attentions.0.group_norm.bias": [512],
"encoder.mid_block.attentions.0.group_norm.weight": [512],
"encoder.mid_block.attentions.0.to_k.bias": [512],
"encoder.mid_block.attentions.0.to_k.weight": [512, 512],
"encoder.mid_block.attentions.0.to_out.0.bias": [512],
"encoder.mid_block.attentions.0.to_out.0.weight": [512, 512],
"encoder.mid_block.attentions.0.to_q.bias": [512],
"encoder.mid_block.attentions.0.to_q.weight": [512, 512],
"encoder.mid_block.attentions.0.to_v.bias": [512],
"encoder.mid_block.attentions.0.to_v.weight": [512, 512],
"encoder.mid_block.resnets.0.conv1.bias": [512],
"encoder.mid_block.resnets.0.conv1.weight": [512, 512, 3, 3],
"encoder.mid_block.resnets.0.conv2.bias": [512],
"encoder.mid_block.resnets.0.conv2.weight": [512, 512, 3, 3],
"encoder.mid_block.resnets.0.norm1.bias": [512],
"encoder.mid_block.resnets.0.norm1.weight": [512],
"encoder.mid_block.resnets.0.norm2.bias": [512],
"encoder.mid_block.resnets.0.norm2.weight": [512],
"encoder.mid_block.resnets.1.conv1.bias": [512],
"encoder.mid_block.resnets.1.conv1.weight": [512, 512, 3, 3],
"encoder.mid_block.resnets.1.conv2.bias": [512],
"encoder.mid_block.resnets.1.conv2.weight": [512, 512, 3, 3],
"encoder.mid_block.resnets.1.norm1.bias": [512],
"encoder.mid_block.resnets.1.norm1.weight": [512],
"encoder.mid_block.resnets.1.norm2.bias": [512],
"encoder.mid_block.resnets.1.norm2.weight": [512],
"post_quant_conv.bias": [32],
"post_quant_conv.weight": [32, 32, 1, 1],
"quant_conv.bias": [64],
"quant_conv.weight": [64, 64, 1, 1],
}
@@ -0,0 +1,77 @@
"""Representative key layout of a ComfyUI single-file Qwen2.5-VL encoder checkpoint.
Captured from `qwen_2.5_vl_7b_fp8_scaled.safetensors` (Qwen2.5-VL-7B, ComfyUI fp8_scaled).
The full checkpoint has 1446 tensors (32 visual blocks, 28 language layers); this
fixture keeps every top-level/structural key plus block 0 of each repeated stack, which is
enough to exercise the `visual.* -> model.visual.*` / `model.* -> model.language_model.*`
remap and the fp8 metadata stripping without shipping a ~1446-key dict.
Legacy ComfyUI layout uses `visual.*`, `model.*`, `lm_head.*` (transformers >=4.50 expects
`model.visual.*` and `model.language_model.*`). `scale_weight` / `scale_input` / `scaled_fp8`
are ComfyUI fp8 quantization metadata.
"""
state_dict_keys: dict[str, list[int]] = {
"visual.blocks.0.attn.proj.bias": [1280],
"visual.blocks.0.attn.proj.scale_input": [],
"visual.blocks.0.attn.proj.scale_weight": [],
"visual.blocks.0.attn.proj.weight": [1280, 1280],
"visual.blocks.0.attn.qkv.bias": [3840],
"visual.blocks.0.attn.qkv.scale_input": [],
"visual.blocks.0.attn.qkv.scale_weight": [],
"visual.blocks.0.attn.qkv.weight": [3840, 1280],
"visual.blocks.0.mlp.down_proj.bias": [1280],
"visual.blocks.0.mlp.down_proj.scale_input": [],
"visual.blocks.0.mlp.down_proj.scale_weight": [],
"visual.blocks.0.mlp.down_proj.weight": [1280, 3420],
"visual.blocks.0.mlp.gate_proj.bias": [3420],
"visual.blocks.0.mlp.gate_proj.scale_input": [],
"visual.blocks.0.mlp.gate_proj.scale_weight": [],
"visual.blocks.0.mlp.gate_proj.weight": [3420, 1280],
"visual.blocks.0.mlp.up_proj.bias": [3420],
"visual.blocks.0.mlp.up_proj.scale_input": [],
"visual.blocks.0.mlp.up_proj.scale_weight": [],
"visual.blocks.0.mlp.up_proj.weight": [3420, 1280],
"visual.blocks.0.norm1.weight": [1280],
"visual.blocks.0.norm2.weight": [1280],
"visual.merger.ln_q.weight": [1280],
"visual.merger.mlp.0.bias": [5120],
"visual.merger.mlp.0.scale_input": [],
"visual.merger.mlp.0.scale_weight": [],
"visual.merger.mlp.0.weight": [5120, 5120],
"visual.merger.mlp.2.bias": [3584],
"visual.merger.mlp.2.scale_input": [],
"visual.merger.mlp.2.scale_weight": [],
"visual.merger.mlp.2.weight": [3584, 5120],
"visual.patch_embed.proj.weight": [1280, 3, 2, 14, 14],
"model.embed_tokens.weight": [152064, 3584],
"model.layers.0.input_layernorm.weight": [3584],
"model.layers.0.mlp.down_proj.scale_input": [],
"model.layers.0.mlp.down_proj.scale_weight": [],
"model.layers.0.mlp.down_proj.weight": [3584, 18944],
"model.layers.0.mlp.gate_proj.scale_input": [],
"model.layers.0.mlp.gate_proj.scale_weight": [],
"model.layers.0.mlp.gate_proj.weight": [18944, 3584],
"model.layers.0.mlp.up_proj.scale_input": [],
"model.layers.0.mlp.up_proj.scale_weight": [],
"model.layers.0.mlp.up_proj.weight": [18944, 3584],
"model.layers.0.post_attention_layernorm.weight": [3584],
"model.layers.0.self_attn.k_proj.bias": [512],
"model.layers.0.self_attn.k_proj.scale_input": [],
"model.layers.0.self_attn.k_proj.scale_weight": [],
"model.layers.0.self_attn.k_proj.weight": [512, 3584],
"model.layers.0.self_attn.o_proj.scale_input": [],
"model.layers.0.self_attn.o_proj.scale_weight": [],
"model.layers.0.self_attn.o_proj.weight": [3584, 3584],
"model.layers.0.self_attn.q_proj.bias": [3584],
"model.layers.0.self_attn.q_proj.scale_input": [],
"model.layers.0.self_attn.q_proj.scale_weight": [],
"model.layers.0.self_attn.q_proj.weight": [3584, 3584],
"model.layers.0.self_attn.v_proj.bias": [512],
"model.layers.0.self_attn.v_proj.scale_input": [],
"model.layers.0.self_attn.v_proj.scale_weight": [],
"model.layers.0.self_attn.v_proj.weight": [512, 3584],
"model.norm.weight": [3584],
"lm_head.weight": [152064, 3584],
"scaled_fp8": [0],
}
@@ -0,0 +1,13 @@
"""Shared helpers for model-loader state-dict fixtures.
Mirrors `tests/backend/patches/lora_conversions/lora_state_dicts/utils.py`: a fixture module
exports `state_dict_keys: dict[str, list[int]]` (key name -> shape, captured from a real
checkpoint) and tests expand it to a mock state dict with `keys_to_mock_state_dict()`.
"""
import torch
def keys_to_mock_state_dict(keys: dict[str, list[int]]) -> dict[str, torch.Tensor]:
"""Build a state dict of empty tensors from a {key: shape} mapping."""
return {k: torch.empty(shape) for k, shape in keys.items()}
@@ -0,0 +1,69 @@
"""Representative ComfyUI key layout of a Z-Image transformer single-file checkpoint.
Captured from `zimageTurboBadmilk_v10.safetensors` (Z-Image Turbo) after stripping the
`model.diffusion_model.` prefix -- this is exactly the input `_convert_z_image_gguf_to_diffusers`
receives (the converter runs on both the checkpoint and GGUF paths after prefix stripping).
The full transformer has 453 keys (context_refiner / noise_refiner / layers stacks); this
fixture keeps block 0 of each stack plus all non-block keys, and adds a synthetic
`norm_final.weight` to exercise the skip branch.
Legacy layout uses fused `*.attention.qkv.*`, `*.attention.out.*`, `*.attention.q_norm/k_norm`,
`x_embedder.*`, `final_layer.*`, `x_pad_token`, `cap_pad_token`; diffusers expects split
`to_q/to_k/to_v`, `to_out.0`, `norm_q/norm_k`, `all_x_embedder.2-1.*`, `all_final_layer.2-1.*`.
"""
state_dict_keys: dict[str, list[int]] = {
"cap_embedder.0.weight": [2560],
"cap_embedder.1.bias": [3840],
"cap_embedder.1.weight": [3840, 2560],
"cap_pad_token": [1, 3840],
"context_refiner.0.attention.k_norm.weight": [128],
"context_refiner.0.attention.out.weight": [3840, 3840],
"context_refiner.0.attention.q_norm.weight": [128],
"context_refiner.0.attention.qkv.weight": [11520, 3840],
"context_refiner.0.attention_norm1.weight": [3840],
"context_refiner.0.attention_norm2.weight": [3840],
"context_refiner.0.feed_forward.w1.weight": [10240, 3840],
"context_refiner.0.feed_forward.w2.weight": [3840, 10240],
"context_refiner.0.feed_forward.w3.weight": [10240, 3840],
"context_refiner.0.ffn_norm1.weight": [3840],
"context_refiner.0.ffn_norm2.weight": [3840],
"final_layer.adaLN_modulation.1.bias": [3840],
"final_layer.adaLN_modulation.1.weight": [3840, 256],
"final_layer.linear.bias": [64],
"final_layer.linear.weight": [64, 3840],
"layers.0.adaLN_modulation.0.bias": [15360],
"layers.0.adaLN_modulation.0.weight": [15360, 256],
"layers.0.attention.k_norm.weight": [128],
"layers.0.attention.out.weight": [3840, 3840],
"layers.0.attention.q_norm.weight": [128],
"layers.0.attention.qkv.weight": [11520, 3840],
"layers.0.attention_norm1.weight": [3840],
"layers.0.attention_norm2.weight": [3840],
"layers.0.feed_forward.w1.weight": [10240, 3840],
"layers.0.feed_forward.w2.weight": [3840, 10240],
"layers.0.feed_forward.w3.weight": [10240, 3840],
"layers.0.ffn_norm1.weight": [3840],
"layers.0.ffn_norm2.weight": [3840],
"noise_refiner.0.adaLN_modulation.0.bias": [15360],
"noise_refiner.0.adaLN_modulation.0.weight": [15360, 256],
"noise_refiner.0.attention.k_norm.weight": [128],
"noise_refiner.0.attention.out.weight": [3840, 3840],
"noise_refiner.0.attention.q_norm.weight": [128],
"noise_refiner.0.attention.qkv.weight": [11520, 3840],
"noise_refiner.0.attention_norm1.weight": [3840],
"noise_refiner.0.attention_norm2.weight": [3840],
"noise_refiner.0.feed_forward.w1.weight": [10240, 3840],
"noise_refiner.0.feed_forward.w2.weight": [3840, 10240],
"noise_refiner.0.feed_forward.w3.weight": [10240, 3840],
"noise_refiner.0.ffn_norm1.weight": [3840],
"noise_refiner.0.ffn_norm2.weight": [3840],
"t_embedder.mlp.0.bias": [1024],
"t_embedder.mlp.0.weight": [1024, 256],
"t_embedder.mlp.2.bias": [256],
"t_embedder.mlp.2.weight": [256, 1024],
"x_embedder.bias": [3840],
"x_embedder.weight": [3840, 64],
"x_pad_token": [1, 3840],
"norm_final.weight": [2304],
}
@@ -0,0 +1,38 @@
"""Unit tests for the Anima single-file prefix-stripping helper."""
import torch
from invokeai.backend.model_manager.load.model_loaders.anima import _strip_anima_bundle_prefix
from tests.backend.model_manager.load.state_dicts.anima_comfyui_keys import state_dict_keys as anima_keys
from tests.backend.model_manager.load.state_dicts.utils import keys_to_mock_state_dict
class TestStripAnimaBundlePrefix:
def test_official_net_prefix_is_stripped(self):
sd = keys_to_mock_state_dict(anima_keys)
assert all(k.startswith("net.") for k in sd)
out = _strip_anima_bundle_prefix(sd)
assert len(out) == len(sd)
assert not any(k.startswith("net.") for k in out)
# Every key had exactly its `net.` prefix removed.
assert {"net." + k for k in out} == set(sd.keys())
def test_comfyui_bundle_keeps_only_transformer_keys(self):
# ComfyUI bundles the transformer under `model.diffusion_model.` alongside the VAE and
# text encoder, which must be dropped.
sd = {
"model.diffusion_model.blocks.0.attn.qkv.weight": torch.empty(1),
"model.diffusion_model.final_layer.weight": torch.empty(1),
"first_stage_model.encoder.conv_in.weight": torch.empty(1),
"cond_stage_model.transformer.embeddings.weight": torch.empty(1),
}
out = _strip_anima_bundle_prefix(sd)
assert set(out.keys()) == {"blocks.0.attn.qkv.weight", "final_layer.weight"}
def test_no_known_prefix_is_a_noop(self):
sd = {"blocks.0.attn.qkv.weight": torch.empty(1)}
assert _strip_anima_bundle_prefix(sd) is sd
@@ -0,0 +1,101 @@
"""Unit tests for the FLUX.2 BFL->diffusers state-dict converters.
Fixtures are captured from real single-file checkpoints (see the fixture module docstrings).
The meta-device tests instantiate the actual diffusers architectures with `init_empty_weights`
(no real weights, no GPU) and assert that every converted key is a real parameter -- the same
kind of check that would have caught the Qwen VL remap regression.
"""
import accelerate
import torch
from invokeai.backend.model_manager.load.model_loaders.flux2_state_dict_utils import (
_flux2_swap_scale_shift,
convert_flux2_bfl_to_diffusers,
convert_flux2_vae_bfl_to_diffusers,
)
from tests.backend.model_manager.load.state_dicts.flux2_transformer_bfl_keys import (
state_dict_keys as flux2_transformer_keys,
)
from tests.backend.model_manager.load.state_dicts.flux2_vae_bfl_keys import (
state_dict_keys as flux2_vae_keys,
)
from tests.backend.model_manager.load.state_dicts.utils import keys_to_mock_state_dict
class TestConvertFlux2Transformer:
def test_fused_qkv_is_split_and_blocks_renamed(self):
sd = keys_to_mock_state_dict(flux2_transformer_keys)
converted = convert_flux2_bfl_to_diffusers(sd)
# Fused img/txt QKV are split into separate projections.
assert "transformer_blocks.0.attn.to_q.weight" in converted
assert "transformer_blocks.0.attn.to_k.weight" in converted
assert "transformer_blocks.0.attn.to_v.weight" in converted
assert "transformer_blocks.0.attn.add_q_proj.weight" in converted
# No fused/BFL-named keys remain.
assert not any("img_attn.qkv" in k or "double_blocks." in k or "single_blocks." in k for k in converted)
# Top-level renames.
assert "x_embedder.weight" in converted
assert "context_embedder.weight" in converted
assert "proj_out.weight" in converted
def test_converted_keys_are_all_real_transformer_params(self):
"""Meta-device coverage: every converted key must exist in Flux2Transformer2DModel."""
from diffusers import Flux2Transformer2DModel
converted = convert_flux2_bfl_to_diffusers(keys_to_mock_state_dict(flux2_transformer_keys))
# The fixture keeps block 0 of each stack -> a single-layer model covers it.
with accelerate.init_empty_weights():
model = Flux2Transformer2DModel(num_layers=1, num_single_layers=1)
params = set(model.state_dict().keys())
unmatched = sorted(k for k in converted if k not in params)
assert not unmatched, f"converted keys with no matching model parameter: {unmatched}"
class TestConvertFlux2Vae:
def test_full_bijective_coverage_against_arch(self):
"""The full VAE fixture must convert to exactly the AutoencoderKLFlux2 parameter set."""
from diffusers import AutoencoderKLFlux2
converted = convert_flux2_vae_bfl_to_diffusers(keys_to_mock_state_dict(flux2_vae_keys))
with accelerate.init_empty_weights():
vae = AutoencoderKLFlux2(block_out_channels=(128, 256, 512, 512))
params = set(vae.state_dict().keys())
unmatched = sorted(k for k in converted if k not in params)
missing = sorted(k for k in params if k not in converted)
assert not unmatched, f"converted keys with no matching VAE parameter: {unmatched}"
assert not missing, f"VAE parameters not covered by the converted checkpoint: {missing}"
def test_up_block_order_is_reversed(self):
# BFL decoder.up.X maps to diffusers up_blocks.(3 - X).
sd = {
"decoder.up.0.block.0.norm1.weight": torch.empty(1),
"decoder.up.3.block.0.norm1.weight": torch.empty(1),
}
converted = convert_flux2_vae_bfl_to_diffusers(sd)
assert "decoder.up_blocks.3.resnets.0.norm1.weight" in converted
assert "decoder.up_blocks.0.resnets.0.norm1.weight" in converted
def test_mid_attention_conv_weights_are_squeezed_to_linear(self):
# BFL stores mid attention as Conv2d [out, in, 1, 1]; diffusers uses Linear [out, in].
sd = {"encoder.mid.attn_1.q.weight": torch.empty(8, 8, 1, 1)}
converted = convert_flux2_vae_bfl_to_diffusers(sd)
assert converted["encoder.mid_block.attentions.0.to_q.weight"].shape == (8, 8)
class TestSwapScaleShift:
def test_swaps_the_two_halves(self):
# First half = shift, second half = scale; diffusers wants them swapped.
weight = torch.cat([torch.zeros(2), torch.ones(2)]) # [shift=0, scale=1]
swapped = _flux2_swap_scale_shift(weight)
assert torch.allclose(swapped, torch.cat([torch.ones(2), torch.zeros(2)]))
def test_leaves_malformed_tensor_untouched(self):
weight = torch.ones(3) # odd length -> cannot be split
assert torch.allclose(_flux2_swap_scale_shift(weight), weight)
@@ -0,0 +1,371 @@
"""Tests for `ModelLoader` FP8 helpers.
Covers:
- `_should_use_fp8` excludes ControlLoRA (the LoRA loader never runs the layerwise
casting helper, and a LoRA isn't a standalone forward module — so a persisted
`fp8_storage=true` must be a no-op).
- `_wrap_forward_with_fp8_cast` uses pre/post hooks with `always_call=True`, so it is
exception-safe AND survives `apply_custom_layers_to_model`'s instance swap. Without
hooks, an instance-level `forward` override would be carried into the new CustomLinear
via the shared `__dict__` and silently bypass `CustomLinear.forward` — breaking LoRA
patch dispatch for FP8 checkpoint models.
- `_apply_fp8_to_nn_module` skips precision-sensitive layers (norm, pos_embed, etc.)
so FLUX RMSNorm.scale and friends aren't crushed to FP8.
"""
from logging import getLogger
from types import SimpleNamespace
from unittest.mock import patch
import pytest
import torch
from invokeai.backend.model_manager.load.load_default import ModelLoader
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_linear import (
CustomLinear,
)
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import (
apply_custom_layers_to_model,
)
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
def _make_loader(device: str = "cuda") -> ModelLoader:
"""Build a ModelLoader without going through dependency injection.
`_should_use_fp8` and `_wrap_forward_with_fp8_cast` only depend on `_torch_device`,
so we instantiate via __new__ and set the minimum state directly.
"""
loader = ModelLoader.__new__(ModelLoader)
loader._torch_device = torch.device(device)
loader._torch_dtype = torch.float16
loader._logger = getLogger("test")
return loader
def _make_config(model_type: ModelType, fp8: bool, base: BaseModelType = BaseModelType.Flux):
return SimpleNamespace(
type=model_type,
base=base,
name="test",
default_settings=SimpleNamespace(fp8_storage=fp8),
)
def test_should_use_fp8_excludes_control_lora():
"""ControlLoRA gets the FP8 toggle in the UI history but the LoRA loader never applies
layerwise casting (the model isn't run as a standalone forward pass — it patches into a
base model). The loader must silently ignore a persisted `fp8_storage=true` to avoid
misleading users who toggled it under a prior version.
"""
loader = _make_loader(device="cuda")
with patch("torch.cuda.is_available", return_value=True):
assert loader._should_use_fp8(_make_config(ModelType.ControlLoRa, fp8=True)) is False
def test_should_use_fp8_excludes_lora():
loader = _make_loader(device="cuda")
assert loader._should_use_fp8(_make_config(ModelType.LoRA, fp8=True)) is False
def test_should_use_fp8_returns_true_for_main_with_fp8():
loader = _make_loader(device="cuda")
assert loader._should_use_fp8(_make_config(ModelType.Main, fp8=True)) is True
def test_should_use_fp8_returns_false_for_main_without_fp8():
loader = _make_loader(device="cuda")
assert loader._should_use_fp8(_make_config(ModelType.Main, fp8=False)) is False
def test_should_use_fp8_returns_false_on_cpu():
loader = _make_loader(device="cpu")
assert loader._should_use_fp8(_make_config(ModelType.Main, fp8=True)) is False
class _RaisingModule(torch.nn.Module):
"""A module whose forward unconditionally raises — used to test that the FP8 wrapper's
storage-dtype cleanup runs even when forward fails."""
def __init__(self):
super().__init__()
self.weight = torch.nn.Parameter(torch.zeros(4))
self.bias = torch.nn.Parameter(torch.zeros(4))
def forward(self, x: torch.Tensor) -> torch.Tensor:
raise RuntimeError("boom")
def _fp8_supported() -> bool:
return hasattr(torch, "float8_e4m3fn")
@pytest.mark.skipif(not _fp8_supported(), reason="torch.float8_e4m3fn not available")
def test_wrap_forward_restores_storage_dtype_on_exception():
"""When forward raises, params must be returned to storage dtype. Otherwise FP8 storage
savings silently revert to fp16/bf16 and the cache's size accounting becomes stale.
"""
storage_dtype = torch.float8_e4m3fn
compute_dtype = torch.bfloat16
module = _RaisingModule()
for p in module.parameters(recurse=False):
p.data = p.data.to(storage_dtype)
ModelLoader._wrap_forward_with_fp8_cast(module, storage_dtype, compute_dtype)
# Sanity: params start in storage dtype.
assert module.weight.dtype == storage_dtype
assert module.bias.dtype == storage_dtype
with pytest.raises(RuntimeError, match="boom"):
module(torch.zeros(4, dtype=compute_dtype))
# Critical assertion: cleanup ran despite the exception.
assert module.weight.dtype == storage_dtype
assert module.bias.dtype == storage_dtype
@pytest.mark.skipif(not _fp8_supported(), reason="torch.float8_e4m3fn not available")
def test_wrap_forward_casts_to_compute_then_back_on_success():
"""Happy-path sanity check: params are in compute dtype during forward, storage dtype after."""
storage_dtype = torch.float8_e4m3fn
compute_dtype = torch.bfloat16
seen_dtypes: list[torch.dtype] = []
class _CaptureModule(torch.nn.Module):
def __init__(self):
super().__init__()
self.weight = torch.nn.Parameter(torch.zeros(4))
def forward(self, x: torch.Tensor) -> torch.Tensor:
seen_dtypes.append(self.weight.dtype)
return x + self.weight
module = _CaptureModule()
for p in module.parameters(recurse=False):
p.data = p.data.to(storage_dtype)
ModelLoader._wrap_forward_with_fp8_cast(module, storage_dtype, compute_dtype)
module(torch.zeros(4, dtype=compute_dtype))
assert seen_dtypes == [compute_dtype]
assert module.weight.dtype == storage_dtype
def test_apply_fp8_to_nn_module_uses_wrapper():
"""`_apply_fp8_to_nn_module` should delegate per-module wrapping to
`_wrap_forward_with_fp8_cast`, which encapsulates the hook registration.
"""
module = torch.nn.Linear(4, 4)
with patch.object(ModelLoader, "_wrap_forward_with_fp8_cast") as mock_wrap:
ModelLoader._apply_fp8_to_nn_module(module, torch.float16, torch.float32)
mock_wrap.assert_called_once_with(module, torch.float16, torch.float32)
def test_apply_fp8_to_nn_module_skips_norm_modules():
"""Modules whose path matches `norm` must not be cast — diffusers' `enable_layerwise_casting`
does the same. FLUX RMSNorm.scale is the canonical example: a tiny learned scalar that
breaks badly in FP8.
"""
class _Model(torch.nn.Module):
def __init__(self):
super().__init__()
self.norm1 = torch.nn.LayerNorm(4)
self.linear = torch.nn.Linear(4, 4)
storage_dtype = torch.float16
compute_dtype = torch.float32
model = _Model()
for p in model.parameters():
p.data = p.data.to(compute_dtype)
ModelLoader._apply_fp8_to_nn_module(model, storage_dtype, compute_dtype)
# Linear params get cast to storage dtype.
assert model.linear.weight.dtype == storage_dtype
# Norm params stay in compute dtype — they must not be cast.
assert model.norm1.weight.dtype == compute_dtype
assert model.norm1.bias.dtype == compute_dtype
def test_apply_fp8_to_nn_module_skips_pos_embed_and_proj_in_out():
"""Position embeddings and the in/out projection of transformer blocks are also on the
diffusers default skip list — they're precision-sensitive.
"""
class _Model(torch.nn.Module):
def __init__(self):
super().__init__()
self.pos_embed = torch.nn.Linear(4, 4)
self.proj_in = torch.nn.Linear(4, 4)
self.proj_out = torch.nn.Linear(4, 4)
self.attn = torch.nn.Linear(4, 4)
storage_dtype = torch.float16
compute_dtype = torch.float32
model = _Model()
for p in model.parameters():
p.data = p.data.to(compute_dtype)
ModelLoader._apply_fp8_to_nn_module(model, storage_dtype, compute_dtype)
assert model.attn.weight.dtype == storage_dtype
assert model.pos_embed.weight.dtype == compute_dtype
assert model.proj_in.weight.dtype == compute_dtype
assert model.proj_out.weight.dtype == compute_dtype
def test_apply_fp8_to_nn_module_skips_unsupported_layer_types():
"""Only the layer classes in `_FP8_SUPPORTED_PYTORCH_LAYERS` are cast — matches diffusers'
behavior. A custom RMSNorm-style module with a raw Parameter must be left alone, otherwise
its learned scalar gets clobbered.
"""
class _ScaleModule(torch.nn.Module):
"""Mimics FLUX RMSNorm — a tiny learned scalar that must not be cast to FP8."""
def __init__(self):
super().__init__()
self.scale = torch.nn.Parameter(torch.ones(4))
def forward(self, x: torch.Tensor) -> torch.Tensor:
return x * self.scale
class _Model(torch.nn.Module):
def __init__(self):
super().__init__()
self.rms = _ScaleModule()
self.linear = torch.nn.Linear(4, 4)
storage_dtype = torch.float16
compute_dtype = torch.float32
model = _Model()
for p in model.parameters():
p.data = p.data.to(compute_dtype)
ModelLoader._apply_fp8_to_nn_module(model, storage_dtype, compute_dtype)
assert model.linear.weight.dtype == storage_dtype
# Critical: the RMS-style scalar lives on a custom module type, not in the supported list.
assert model.rms.scale.dtype == compute_dtype
def test_wrap_forward_reaches_custom_linear_after_apply_custom_layers():
"""Production order: `_load_model` applies FP8 wrapping, THEN `ModelCache.put()` calls
`apply_custom_layers_to_model` which constructs a NEW `CustomLinear` object via
`CustomLinear.__new__` and points its `__dict__` at the original `Linear.__dict__`
(see `wrap_custom_layer`). The new object is installed on the parent in place of the
original Linear.
An instance-level `forward` override would be carried into the new CustomLinear via the
shared dict but would close over the OLD Linear instance — so calls to the new
CustomLinear would silently route to `Linear.forward(old_instance, ...)` and bypass
`CustomLinear.forward`, where LoRA/ControlLoRA patches are applied. This is the bug a
reviewer reproduced on a fresh worktree.
Hooks fix this because `nn.Module._call_impl` dispatches them with the *actual* called
instance, and `self.forward(...)` is resolved by normal class lookup — reaching
`CustomLinear.forward`. This test exercises the production wrapping path (real
`apply_custom_layers_to_model`) and asserts CustomLinear.forward is reached by attaching
a sentinel patch list and observing that the patch-aware branch runs.
"""
class Parent(torch.nn.Module):
def __init__(self):
super().__init__()
self.child = torch.nn.Linear(4, 4, bias=False)
parent = Parent()
original_linear = parent.child
ModelLoader._wrap_forward_with_fp8_cast(original_linear, torch.float16, torch.float32)
apply_custom_layers_to_model(parent)
new_child = parent.child
# Sanity: production wrapping replaced the child with a NEW CustomLinear instance.
assert isinstance(new_child, CustomLinear)
assert new_child is not original_linear
# Attach a sentinel patch so CustomLinear.forward routes through the LoRA-aware branch
# (see custom_linear.py: `if len(self._patches_and_weights) > 0`). If that branch fires,
# our FP8 wrapping is correctly dispatched through CustomLinear.forward.
patch_was_invoked = {"hit": False}
class _SentinelPatch:
def __init__(self):
self.hit = patch_was_invoked
def __call__(self, *_args, **_kwargs): # not actually called
pass
# Patch the CustomLinear's patch-handling branch to record that it was reached.
original_patch_branch = CustomLinear._autocast_forward_with_patches
def tracked_patch_branch(self, input):
patch_was_invoked["hit"] = True
# Return a same-shape tensor so the outer caller doesn't choke.
return torch.zeros_like(input @ self.weight.t())
new_child._patches_and_weights = [(_SentinelPatch(), 1.0)]
try:
CustomLinear._autocast_forward_with_patches = tracked_patch_branch
_ = new_child(torch.zeros(1, 4, dtype=torch.float32))
finally:
CustomLinear._autocast_forward_with_patches = original_patch_branch
new_child._patches_and_weights = []
assert patch_was_invoked["hit"] is True, (
"FP8-wrapped forward did not reach CustomLinear.forward — LoRA/ControlLoRA patches "
"would be silently bypassed on FP8 checkpoint models."
)
def test_apply_fp8_layerwise_casting_uses_hook_path_for_model_mixin():
"""Regression test for the FLUX.2 Klein 9B partial-load device-mismatch crash.
Diffusers' `enable_layerwise_casting()` registers a `LayerwiseCastingHook` whose
`pre_forward` only casts dtype (not device) and whose hook system replaces
`Linear.forward` with a wrapper that calls the *original* `Linear.forward` captured
before the hook was installed. `ModelCache.put()` later wraps Linear as CustomLinear
sharing `__dict__`, so the diffusers wrapper is carried into the new CustomLinear and
routes calls to the captured original Linear.forward — bypassing
`CustomLinear.forward`'s `cast_to_device`. On partial load (some weights on CPU,
input on cuda), this raises a device-mismatch error.
The fix routes ModelMixin through `_apply_fp8_to_nn_module` (hook-based,
`forward`-preserving). This test asserts that path is taken even when the model
inherits from ModelMixin.
"""
from diffusers.models.modeling_utils import ModelMixin
class _FakeModelMixin(ModelMixin):
# ModelMixin requires a config_name class attribute and a config dict for serialization.
# We never serialize, so we only need to satisfy isinstance() checks.
config_name = "config.json"
def __init__(self):
super().__init__()
self.linear = torch.nn.Linear(4, 4, bias=False)
def forward(self, x):
return self.linear(x)
loader = _make_loader(device="cuda")
config = _make_config(ModelType.Main, fp8=True)
model = _FakeModelMixin()
with (
patch.object(ModelLoader, "_should_use_fp8", return_value=True),
patch.object(ModelLoader, "_apply_fp8_to_nn_module") as mock_to_nn,
patch.object(_FakeModelMixin, "enable_layerwise_casting") as mock_enable,
):
loader._apply_fp8_layerwise_casting(model, config)
mock_to_nn.assert_called_once()
mock_enable.assert_not_called()
@@ -0,0 +1,82 @@
import pytest
import torch
from invokeai.backend.model_manager.load.load_base import LoadedModelWithoutConfig
from invokeai.backend.model_manager.load.model_cache.cache_record import CacheRecord
from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_only_full_load import (
CachedModelOnlyFullLoad,
)
from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_with_partial_load import (
CachedModelWithPartialLoad,
)
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import (
apply_custom_layers_to_model,
)
class ModelWithRequiredScale(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear = torch.nn.Linear(4, 4)
self.scale = torch.nn.Parameter(torch.ones(4))
class FakeCache:
def __init__(self):
self.lock_calls = 0
self.unlock_calls = 0
def lock(self, cache_record: CacheRecord, working_mem_bytes: int | None) -> None:
del cache_record, working_mem_bytes
self.lock_calls += 1
def unlock(self, cache_record: CacheRecord) -> None:
del cache_record
self.unlock_calls += 1
def test_model_on_device_repairs_required_tensors_for_partial_models():
model = ModelWithRequiredScale()
apply_custom_layers_to_model(model, device_autocasting_enabled=True)
cached_model = CachedModelWithPartialLoad(model=model, compute_device=torch.device("meta"), keep_ram_copy=False)
loaded_model = LoadedModelWithoutConfig(
cache_record=CacheRecord(key="test", cached_model=cached_model), cache=FakeCache()
)
with loaded_model.model_on_device():
assert model.scale.device.type == "meta"
assert all(param.device.type == "cpu" for param in model.linear.parameters())
def test_model_on_device_leaves_full_load_models_unchanged():
model = torch.nn.Linear(4, 4)
cached_model = CachedModelOnlyFullLoad(
model=model, compute_device=torch.device("meta"), total_bytes=1, keep_ram_copy=False
)
loaded_model = LoadedModelWithoutConfig(
cache_record=CacheRecord(key="test", cached_model=cached_model), cache=FakeCache()
)
with loaded_model.model_on_device() as (_, returned_model):
assert returned_model is model
assert all(param.device.type == "cpu" for param in model.parameters())
def test_enter_unlocks_if_repair_raises():
class BrokenCachedModel(CachedModelWithPartialLoad):
def repair_required_tensors_on_compute_device(self) -> int:
raise RuntimeError("repair failed")
model = ModelWithRequiredScale()
apply_custom_layers_to_model(model, device_autocasting_enabled=True)
cached_model = BrokenCachedModel(model=model, compute_device=torch.device("meta"), keep_ram_copy=False)
fake_cache = FakeCache()
loaded_model = LoadedModelWithoutConfig(
cache_record=CacheRecord(key="test", cached_model=cached_model), cache=fake_cache
)
with pytest.raises(RuntimeError, match="repair failed"):
loaded_model.__enter__()
assert fake_cache.lock_calls == 1
assert fake_cache.unlock_calls == 1
@@ -0,0 +1,169 @@
"""Unit tests for the pure state-dict helpers in the Qwen-Image / Qwen-VL loader.
These freeze the checkpoint key-surgery that the loaders perform before instantiating a model,
so a regression like the transformers-5.x one (where `_checkpoint_conversion_mapping` became
`{}` and the `visual.* -> model.visual.*` remap was silently skipped) fails here instead of at
the user's first load.
"""
import torch
from invokeai.backend.model_manager.load.model_loaders.qwen_image import (
_build_qwen_image_transformer_config,
_dequantize_comfyui_fp8,
_remap_qwen_vl_checkpoint_keys,
_strip_comfyui_prefix,
_strip_quantization_metadata,
)
from tests.backend.model_manager.load.state_dicts.qwen_vl_encoder_comfyui_keys import (
state_dict_keys as qwen_vl_keys,
)
from tests.backend.model_manager.load.state_dicts.utils import keys_to_mock_state_dict
# Prefixes the Qwen2.5-VL architecture (transformers >=4.50) actually exposes. Frozen here on
# purpose: if a remap regresses, converted keys stop matching this set.
_VALID_QWEN_VL_PREFIXES = ("model.visual.", "model.language_model.", "lm_head")
class TestRemapQwenVlCheckpointKeys:
def test_every_key_maps_to_the_transformers_layout(self):
"""Every legacy ComfyUI key must land under `model.visual.*` / `model.language_model.*`."""
sd = keys_to_mock_state_dict(qwen_vl_keys)
# The loader strips fp8 metadata (`scaled_fp8` etc.) before remapping; mirror that order.
_strip_quantization_metadata(sd)
remapped = _remap_qwen_vl_checkpoint_keys(sd)
assert len(remapped) == len(sd)
for key in remapped:
assert key.startswith(_VALID_QWEN_VL_PREFIXES), f"key not remapped to a known layout: {key}"
# No key may survive in the legacy layout.
assert not any(k.startswith("visual.") for k in remapped)
assert not any(k.startswith(("model.layers.", "model.embed_tokens", "model.norm")) for k in remapped)
def test_specific_keys_from_the_bug_report(self):
"""The exact keys that failed to load in the original bug report are remapped."""
sd = {
"visual.blocks.0.attn.qkv.weight": torch.empty(1),
"visual.patch_embed.proj.weight": torch.empty(1),
"model.layers.0.self_attn.q_proj.weight": torch.empty(1),
"model.embed_tokens.weight": torch.empty(1),
"lm_head.weight": torch.empty(1),
}
remapped = _remap_qwen_vl_checkpoint_keys(sd)
assert "model.visual.blocks.0.attn.qkv.weight" in remapped
assert "model.visual.patch_embed.proj.weight" in remapped
assert "model.language_model.layers.0.self_attn.q_proj.weight" in remapped
assert "model.language_model.embed_tokens.weight" in remapped
assert "lm_head.weight" in remapped # unchanged
def test_idempotent_on_already_converted_layout(self):
"""Re-running the remap on new-layout keys must not double-prefix them."""
sd = keys_to_mock_state_dict(qwen_vl_keys)
once = _remap_qwen_vl_checkpoint_keys(sd)
twice = _remap_qwen_vl_checkpoint_keys(once)
assert set(once.keys()) == set(twice.keys())
def test_fallback_when_transformers_mapping_is_empty(self, monkeypatch):
"""Even if transformers stops providing `_checkpoint_conversion_mapping`, the remap fires.
transformers 5.x returns `{}` here; forcing that value pins the fallback that fixes the
original bug.
"""
from transformers import Qwen2_5_VLForConditionalGeneration
monkeypatch.setattr(Qwen2_5_VLForConditionalGeneration, "_checkpoint_conversion_mapping", {})
remapped = _remap_qwen_vl_checkpoint_keys(
{
"visual.blocks.0.norm1.weight": torch.empty(1),
"model.layers.0.input_layernorm.weight": torch.empty(1),
}
)
assert "model.visual.blocks.0.norm1.weight" in remapped
assert "model.language_model.layers.0.input_layernorm.weight" in remapped
class TestStripQuantizationMetadata:
def test_drops_fp8_metadata_keeps_weights(self):
sd = keys_to_mock_state_dict(qwen_vl_keys)
# The captured checkpoint is fp8_scaled, so it really does ship this metadata.
assert any(k.endswith((".scale_weight", ".scale_input")) or k == "scaled_fp8" for k in sd)
n_weights_before = sum(1 for k in sd if k.endswith(".weight"))
_strip_quantization_metadata(sd)
assert not any(
k.endswith((".scale_weight", ".scale_input")) or "comfy_quant" in k or k == "scaled_fp8" for k in sd
)
# Real weights are untouched.
assert sum(1 for k in sd if k.endswith(".weight")) == n_weights_before
class TestDequantizeComfyuiFp8:
def test_scalar_scale(self):
sd = {
"l.weight": torch.full((2, 2), 2.0),
"l.scale_weight": torch.tensor(3.0),
"l.scale_input": torch.tensor(9.0), # activation scale, must be ignored
}
count = _dequantize_comfyui_fp8(sd, torch.float32)
assert count == 1
assert torch.allclose(sd["l.weight"], torch.full((2, 2), 6.0))
def test_block_wise_scale_is_broadcast(self):
# Per-block scale [2, 1] must be repeat_interleaved up to the weight shape [4, 2].
sd = {
"l.weight": torch.ones(4, 2),
"l.weight_scale": torch.tensor([[10.0], [20.0]]),
}
count = _dequantize_comfyui_fp8(sd, torch.float32)
assert count == 1
expected = torch.tensor([[10.0, 10.0], [10.0, 10.0], [20.0, 20.0], [20.0, 20.0]])
assert torch.allclose(sd["l.weight"], expected)
class TestStripComfyuiPrefix:
def test_strips_diffusion_model_prefix(self):
sd = {
"model.diffusion_model.transformer_blocks.0.img_mod.1.weight": torch.empty(1),
"model.diffusion_model.img_in.weight": torch.empty(1),
}
out = _strip_comfyui_prefix(sd)
assert set(out.keys()) == {"transformer_blocks.0.img_mod.1.weight", "img_in.weight"}
def test_no_prefix_is_a_noop(self):
sd = {"transformer_blocks.0.x": torch.empty(1)}
assert _strip_comfyui_prefix(sd) is sd
class TestBuildQwenImageTransformerConfig:
def test_infers_layer_count_and_dims_from_shapes(self):
# torch-order (logical) shapes, as the GGMLTensor.tensor_shape / safetensors path exposes.
sd = {
"img_in.weight": torch.empty(3072, 64),
"txt_in.weight": torch.empty(3072, 3584),
"transformer_blocks.0.img_mod.1.weight": torch.empty(1),
"transformer_blocks.1.img_mod.1.weight": torch.empty(1),
"transformer_blocks.5.img_mod.1.weight": torch.empty(1),
}
cfg = _build_qwen_image_transformer_config(sd, is_edit=False)
assert cfg["num_layers"] == 6 # max block index (5) + 1
assert cfg["in_channels"] == 64
assert cfg["num_attention_heads"] == 24 # 3072 // 128
assert cfg["joint_attention_dim"] == 3584
def test_empty_state_dict_falls_back_to_defaults(self):
cfg = _build_qwen_image_transformer_config({}, is_edit=False)
assert cfg["num_layers"] == 60
@@ -0,0 +1,66 @@
"""Unit tests for the Z-Image GGUF/ComfyUI -> diffusers state-dict converter."""
import torch
from invokeai.backend.model_manager.load.model_loaders.z_image import _convert_z_image_gguf_to_diffusers
from tests.backend.model_manager.load.state_dicts.utils import keys_to_mock_state_dict
from tests.backend.model_manager.load.state_dicts.z_image_transformer_comfyui_keys import (
state_dict_keys as z_image_keys,
)
class TestConvertZImageGgufToDiffusers:
def test_fused_qkv_split(self):
sd = keys_to_mock_state_dict(z_image_keys)
n_qkv = sum(1 for k in sd if k.endswith(".attention.qkv.weight"))
assert n_qkv > 0
out = _convert_z_image_gguf_to_diffusers(sd)
# Each fused qkv weight becomes three separate projections.
assert sum(1 for k in out if k.endswith(".attention.to_q.weight")) == n_qkv
assert sum(1 for k in out if k.endswith(".attention.to_k.weight")) == n_qkv
assert sum(1 for k in out if k.endswith(".attention.to_v.weight")) == n_qkv
assert not any(".attention.qkv." in k for k in out)
def test_key_renames(self):
out = _convert_z_image_gguf_to_diffusers(keys_to_mock_state_dict(z_image_keys))
# q_norm/k_norm -> norm_q/norm_k, attention.out -> attention.to_out.0
assert any(k.endswith(".attention.norm_q.weight") for k in out)
assert any(k.endswith(".attention.norm_k.weight") for k in out)
assert any(k.endswith(".attention.to_out.0.weight") for k in out)
assert not any(".q_norm." in k or ".k_norm." in k for k in out)
assert not any(".attention.out." in k for k in out)
def test_embedder_and_final_layer_renamed(self):
out = _convert_z_image_gguf_to_diffusers(keys_to_mock_state_dict(z_image_keys))
assert any(k.startswith("all_x_embedder.2-1.") for k in out)
assert any(k.startswith("all_final_layer.2-1.") for k in out)
assert not any(k.startswith("x_embedder.") or k.startswith("final_layer.") for k in out)
def test_norm_final_is_dropped(self):
# The diffusers model uses a non-learnable final LayerNorm, so norm_final.* is skipped.
assert any(k.startswith("norm_final.") for k in z_image_keys)
out = _convert_z_image_gguf_to_diffusers(keys_to_mock_state_dict(z_image_keys))
assert not any(k.startswith("norm_final.") for k in out)
def test_pad_tokens_are_2d_after_conversion(self):
# The diffusers model expects a leading batch dim on the pad tokens. The checkpoint
# already stores them 2D; GGUF ships them 1D (see the reshape test below).
out = _convert_z_image_gguf_to_diffusers(keys_to_mock_state_dict(z_image_keys))
for pad in ("x_pad_token", "cap_pad_token"):
assert out[pad].dim() == 2
assert out[pad].shape[0] == 1
def test_1d_pad_token_gains_batch_dim(self):
# GGUF stores pad tokens as [dim]; they must be reshaped to [1, dim].
out = _convert_z_image_gguf_to_diffusers({"x_pad_token": torch.arange(4.0)})
assert out["x_pad_token"].shape == (1, 4)
def test_qkv_split_preserves_values(self):
# A [6, 2] fused qkv splits into three [2, 2] chunks in order q, k, v.
qkv = torch.arange(12, dtype=torch.float32).reshape(6, 2)
out = _convert_z_image_gguf_to_diffusers({"blk.attention.qkv.weight": qkv})
assert torch.allclose(out["blk.attention.to_q.weight"], qkv[0:2])
assert torch.allclose(out["blk.attention.to_k.weight"], qkv[2:4])
assert torch.allclose(out["blk.attention.to_v.weight"], qkv[4:6])
@@ -0,0 +1,25 @@
"""
Test model loading
"""
from pathlib import Path
from invokeai.app.services.model_manager import ModelManagerServiceBase
from invokeai.backend.textual_inversion import TextualInversionModelRaw
def test_loading(mm2_model_manager: ModelManagerServiceBase, embedding_file: Path):
store = mm2_model_manager.store
matches = store.search_by_attr(model_name="test_embedding")
assert len(matches) == 0
key = mm2_model_manager.install.register_path(embedding_file)
loaded_model = mm2_model_manager.load.load_model(store.get_model(key))
assert loaded_model is not None
assert loaded_model.config.key == key
with loaded_model as model:
assert isinstance(model, TextualInversionModelRaw)
config = mm2_model_manager.store.get_model(key)
loaded_model_2 = mm2_model_manager.load.load_model(config)
assert loaded_model.config.key == loaded_model_2.config.key
@@ -0,0 +1,359 @@
# Fixtures to support testing of the model_manager v2 installer, metadata and record store
import os
import shutil
from pathlib import Path
import pytest
from requests.sessions import Session
from requests_testadapter import TestAdapter, TestSession
from invokeai.app.services.config import InvokeAIAppConfig
from invokeai.app.services.download import DownloadQueueService, DownloadQueueServiceBase
from invokeai.app.services.model_install import ModelInstallService, ModelInstallServiceBase
from invokeai.app.services.model_load import ModelLoadService, ModelLoadServiceBase
from invokeai.app.services.model_manager import ModelManagerService, ModelManagerServiceBase
from invokeai.app.services.model_records import ModelRecordServiceBase, ModelRecordServiceSQL
from invokeai.backend.model_manager.configs.lora import LoRA_Diffusers_SD1_Config, LoRA_Diffusers_SDXL_Config
from invokeai.backend.model_manager.configs.main import Main_Checkpoint_SD1_Config, Main_Diffusers_SDXL_Config
from invokeai.backend.model_manager.configs.vae import VAE_Diffusers_SD1_Config
from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache
from invokeai.backend.model_manager.taxonomy import (
BaseModelType,
ModelFormat,
ModelRepoVariant,
ModelSourceType,
ModelType,
ModelVariantType,
SchedulerPredictionType,
)
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.logging import InvokeAILogger
from tests.backend.model_manager.model_metadata.metadata_examples import (
HFTestLoraMetadata,
RepoCivitaiModelMetadata1,
RepoCivitaiVersionMetadata1,
RepoHFMetadata1,
RepoHFMetadata1_nofp16,
RepoHFModelJson1,
)
from tests.fixtures.sqlite_database import create_mock_sqlite_database
from tests.test_nodes import TestEventService
# Create a temporary directory using the contents of `./data/invokeai_root` as the template
@pytest.fixture
def mm2_root_dir(tmp_path_factory) -> Path:
root_template = Path(__file__).resolve().parent / "data" / "invokeai_root"
temp_dir: Path = tmp_path_factory.mktemp("data") / "invokeai_root"
shutil.copytree(root_template, temp_dir)
return temp_dir
@pytest.fixture
def mm2_model_files(tmp_path_factory) -> Path:
root_template = Path(__file__).resolve().parent / "data" / "test_files"
temp_dir: Path = tmp_path_factory.mktemp("data") / "test_files"
shutil.copytree(root_template, temp_dir)
return temp_dir
@pytest.fixture
def embedding_file(mm2_model_files: Path) -> Path:
return mm2_model_files / "test_embedding.safetensors"
# Can be used to test diffusers model directory loading, but
# the test file adds ~10MB of space.
# @pytest.fixture
# def vae_directory(mm2_model_files: Path) -> Path:
# return mm2_model_files / "taesdxl"
@pytest.fixture
def diffusers_dir(mm2_model_files: Path) -> Path:
return mm2_model_files / "test-diffusers-main"
@pytest.fixture
def mm2_app_config(mm2_root_dir: Path) -> InvokeAIAppConfig:
app_config = InvokeAIAppConfig(models_dir=mm2_root_dir / "models", log_level="info", allow_unknown_models=False)
app_config._root = mm2_root_dir
return app_config
@pytest.fixture
def mm2_download_queue(mm2_session: Session) -> DownloadQueueServiceBase:
download_queue = DownloadQueueService(requests_session=mm2_session)
download_queue.start()
yield download_queue
download_queue.stop()
@pytest.fixture
def mm2_loader(mm2_app_config: InvokeAIAppConfig) -> ModelLoadServiceBase:
ram_cache = ModelCache(
execution_device_working_mem_gb=mm2_app_config.device_working_mem_gb,
enable_partial_loading=mm2_app_config.enable_partial_loading,
keep_ram_copy_of_weights=mm2_app_config.keep_ram_copy_of_weights,
max_ram_cache_size_gb=mm2_app_config.max_cache_ram_gb,
max_vram_cache_size_gb=mm2_app_config.max_cache_vram_gb,
execution_device=TorchDevice.choose_torch_device(),
logger=InvokeAILogger.get_logger(),
)
return ModelLoadService(
app_config=mm2_app_config,
ram_cache=ram_cache,
)
@pytest.fixture
def mm2_installer(
mm2_app_config: InvokeAIAppConfig,
mm2_download_queue: DownloadQueueServiceBase,
mm2_session: Session,
) -> ModelInstallServiceBase:
logger = InvokeAILogger.get_logger()
db = create_mock_sqlite_database(mm2_app_config, logger)
events = TestEventService()
store = ModelRecordServiceSQL(db, logger)
installer = ModelInstallService(
app_config=mm2_app_config,
record_store=store,
download_queue=mm2_download_queue,
event_bus=events,
session=mm2_session,
)
installer.start()
yield installer
installer.stop()
@pytest.fixture
def mm2_record_store(mm2_app_config: InvokeAIAppConfig) -> ModelRecordServiceBase:
logger = InvokeAILogger.get_logger(config=mm2_app_config)
db = create_mock_sqlite_database(mm2_app_config, logger)
store = ModelRecordServiceSQL(db, logger)
# add five simple config records to the database
config1 = VAE_Diffusers_SD1_Config(
key="test_config_1",
path="/tmp/foo1",
format=ModelFormat.Diffusers,
name="test2",
base=BaseModelType.StableDiffusion1,
type=ModelType.VAE,
hash="111222333444",
file_size=4096,
source="stabilityai/sdxl-vae",
source_type=ModelSourceType.HFRepoID,
repo_variant=ModelRepoVariant.Default,
)
config2 = Main_Checkpoint_SD1_Config(
key="test_config_2",
path="/tmp/foo2.ckpt",
name="model1",
format=ModelFormat.Checkpoint,
base=BaseModelType.StableDiffusion1,
type=ModelType.Main,
config_path="/tmp/foo.yaml",
variant=ModelVariantType.Normal,
hash="111222333444",
file_size=8192,
source="https://civitai.com/models/206883/split",
source_type=ModelSourceType.Url,
prediction_type=SchedulerPredictionType.Epsilon,
)
config3 = Main_Diffusers_SDXL_Config(
key="test_config_3",
path="/tmp/foo3",
format=ModelFormat.Diffusers,
name="test3",
base=BaseModelType.StableDiffusionXL,
type=ModelType.Main,
hash="111222333444",
file_size=8193,
source="author3/model3",
description="This is test 3",
source_type=ModelSourceType.HFRepoID,
variant=ModelVariantType.Normal,
prediction_type=SchedulerPredictionType.Epsilon,
repo_variant=ModelRepoVariant.Default,
)
config4 = LoRA_Diffusers_SDXL_Config(
key="test_config_4",
path="/tmp/foo4",
format=ModelFormat.Diffusers,
name="test4",
base=BaseModelType.StableDiffusionXL,
type=ModelType.LoRA,
hash="111222333444",
file_size=5000,
source="author4/model4",
source_type=ModelSourceType.HFRepoID,
)
config5 = LoRA_Diffusers_SD1_Config(
key="test_config_5",
path="/tmp/foo5",
format=ModelFormat.Diffusers,
name="test5",
base=BaseModelType.StableDiffusion1,
type=ModelType.LoRA,
hash="111222333444",
file_size=5001,
source="author4/model5",
source_type=ModelSourceType.HFRepoID,
)
store.add_model(config1)
store.add_model(config2)
store.add_model(config3)
store.add_model(config4)
store.add_model(config5)
return store
@pytest.fixture
def mm2_model_manager(
mm2_record_store: ModelRecordServiceBase, mm2_installer: ModelInstallServiceBase, mm2_loader: ModelLoadServiceBase
) -> ModelManagerServiceBase:
return ModelManagerService(store=mm2_record_store, install=mm2_installer, load=mm2_loader)
@pytest.fixture
def mm2_session(embedding_file: Path, diffusers_dir: Path) -> Session:
"""This fixtures defines a series of mock URLs for testing download and installation."""
sess: Session = TestSession()
sess.mount(
"https://test.com/missing_model.safetensors",
TestAdapter(
b"missing",
status=404,
),
)
sess.mount(
"https://huggingface.co/api/models/stabilityai/sdxl-turbo",
TestAdapter(
RepoHFMetadata1,
headers={"Content-Type": "application/json; charset=utf-8", "Content-Length": len(RepoHFMetadata1)},
),
)
sess.mount(
"https://huggingface.co/api/models/stabilityai/sdxl-turbo-nofp16",
TestAdapter(
RepoHFMetadata1_nofp16,
headers={"Content-Type": "application/json; charset=utf-8", "Content-Length": len(RepoHFMetadata1_nofp16)},
),
)
sess.mount(
"https://civitai.com/api/v1/model-versions/242807",
TestAdapter(
RepoCivitaiVersionMetadata1,
headers={
"Content-Length": len(RepoCivitaiVersionMetadata1),
},
),
)
sess.mount(
"https://civitai.com/api/v1/models/215485",
TestAdapter(
RepoCivitaiModelMetadata1,
headers={
"Content-Length": len(RepoCivitaiModelMetadata1),
},
),
)
sess.mount(
"https://huggingface.co/stabilityai/sdxl-turbo/resolve/main/model_index.json",
TestAdapter(
RepoHFModelJson1,
headers={
"Content-Length": len(RepoHFModelJson1),
},
),
)
with open(embedding_file, "rb") as f:
data = f.read() # file is small - just 15K
sess.mount(
"https://www.test.foo/download/test_embedding.safetensors",
TestAdapter(data, headers={"Content-Type": "application/octet-stream", "Content-Length": len(data)}),
)
sess.mount(
"https://huggingface.co/api/models/stabilityai/sdxl-turbo",
TestAdapter(
RepoHFMetadata1,
headers={"Content-Type": "application/json; charset=utf-8", "Content-Length": len(RepoHFMetadata1)},
),
)
sess.mount(
"https://huggingface.co/api/models/InvokeAI-test/textual_inversion_tests?blobs=True",
TestAdapter(
HFTestLoraMetadata,
headers={"Content-Type": "application/json; charset=utf-8", "Content-Length": len(HFTestLoraMetadata)},
),
)
sess.mount(
"https://huggingface.co/InvokeAI-test/textual_inversion_tests/resolve/main/learned_embeds-steps-1000.safetensors",
TestAdapter(
data,
headers={"Content-Type": "application/json; charset=utf-8", "Content-Length": len(data)},
),
)
for root, _, files in os.walk(diffusers_dir):
for name in files:
path = Path(root, name)
url_base = path.relative_to(diffusers_dir).as_posix()
url = f"https://huggingface.co/stabilityai/sdxl-turbo/resolve/main/{url_base}"
with open(path, "rb") as f:
data = f.read()
sess.mount(
url,
TestAdapter(
data,
headers={
"Content-Type": "application/json; charset=utf-8",
"Content-Length": len(data),
},
),
)
for i in ["12345", "9999", "54321"]:
content = (
b"I am a safetensors file " + bytearray(i, "utf-8") + bytearray(32_000)
) # for pause tests, must make content large
sess.mount(
f"http://www.civitai.com/models/{i}",
TestAdapter(
content,
headers={
"Content-Length": len(content),
"Content-Disposition": f'filename="mock{i}.safetensors"',
},
),
)
sess.mount(
"http://www.huggingface.co/foo.txt",
TestAdapter(
content,
headers={
"Content-Length": len(content),
"Content-Disposition": 'filename="foo.safetensors"',
},
),
)
# here are some malformed URLs to test
# missing the content length
sess.mount(
"http://www.civitai.com/models/missing",
TestAdapter(
b"Missing content length",
headers={
"Content-Disposition": 'filename="missing.txt"',
},
),
)
# not found test
sess.mount("http://www.civitai.com/models/broken", TestAdapter(b"Not found", status=404))
return sess
File diff suppressed because one or more lines are too long
@@ -0,0 +1,54 @@
import pytest
from pydantic import ValidationError
from invokeai.backend.model_manager.configs.external_api import (
ExternalApiModelConfig,
ExternalApiModelDefaultSettings,
ExternalImageSize,
ExternalModelCapabilities,
)
def test_external_api_model_config_defaults() -> None:
capabilities = ExternalModelCapabilities(modes=["txt2img"], supports_seed=True)
config = ExternalApiModelConfig(
name="Test External",
provider_id="openai",
provider_model_id="gpt-image-1",
capabilities=capabilities,
)
assert config.path == "external://openai/gpt-image-1"
assert config.source == "external://openai/gpt-image-1"
assert config.hash == "external:openai:gpt-image-1"
assert config.file_size == 0
assert config.default_settings is None
assert config.capabilities.supports_seed is True
def test_external_api_model_capabilities_allows_aspect_ratio_sizes() -> None:
capabilities = ExternalModelCapabilities(
modes=["txt2img"],
allowed_aspect_ratios=["1:1"],
aspect_ratio_sizes={"1:1": ExternalImageSize(width=1024, height=1024)},
)
assert capabilities.aspect_ratio_sizes is not None
assert capabilities.aspect_ratio_sizes["1:1"].width == 1024
def test_external_api_model_config_rejects_extra_fields() -> None:
with pytest.raises(ValidationError):
ExternalModelCapabilities(modes=["txt2img"], supports_seed=True, extra_field=True) # type: ignore
with pytest.raises(ValidationError):
ExternalApiModelDefaultSettings(width=512, extra_field=True) # type: ignore
def test_external_api_model_config_validates_limits() -> None:
with pytest.raises(ValidationError):
ExternalModelCapabilities(modes=["txt2img"], max_images_per_request=0)
with pytest.raises(ValidationError):
ExternalApiModelDefaultSettings(width=0)
@@ -0,0 +1,27 @@
import pytest
from invokeai.backend.model_manager.util.libc_util import LibcUtil, Struct_mallinfo2
def test_libc_util_mallinfo2():
"""Smoke test of LibcUtil().mallinfo2()."""
try:
libc = LibcUtil()
except OSError:
# TODO: Set the expected result preemptively based on the system properties.
pytest.xfail("libc shared library is not available on this system.")
try:
info = libc.mallinfo2()
except AttributeError:
pytest.xfail("`mallinfo2` is not available on this system, likely due to glibc < 2.33.")
assert info.arena > 0
def test_struct_mallinfo2_to_str():
"""Smoke test of Struct_mallinfo2.__str__()."""
info = Struct_mallinfo2()
info_str = str(info)
assert len(info_str) > 0
@@ -0,0 +1,39 @@
import pytest
from invokeai.backend.model_manager.load.memory_snapshot import MemorySnapshot, get_pretty_snapshot_diff
from invokeai.backend.model_manager.util.libc_util import Struct_mallinfo2
def test_memory_snapshot_capture():
"""Smoke test of MemorySnapshot.capture()."""
snapshot = MemorySnapshot.capture()
# We just check process_ram, because it is the only field that should be supported on all platforms.
assert snapshot.process_ram > 0
snapshots = [
MemorySnapshot(process_ram=1, vram=2, malloc_info=Struct_mallinfo2()),
MemorySnapshot(process_ram=1, vram=2, malloc_info=None),
MemorySnapshot(process_ram=1, vram=None, malloc_info=Struct_mallinfo2()),
MemorySnapshot(process_ram=1, vram=None, malloc_info=None),
None,
]
@pytest.mark.parametrize("snapshot_1", snapshots)
@pytest.mark.parametrize("snapshot_2", snapshots)
def test_get_pretty_snapshot_diff(snapshot_1, snapshot_2):
"""Test that get_pretty_snapshot_diff() works with various combinations of missing MemorySnapshot fields."""
msg = get_pretty_snapshot_diff(snapshot_1, snapshot_2)
print(msg)
expected_lines = 0
if snapshot_1 is not None and snapshot_2 is not None:
expected_lines += 1
if snapshot_1.vram is not None and snapshot_2.vram is not None:
expected_lines += 1
if snapshot_1.malloc_info is not None and snapshot_2.malloc_info is not None:
expected_lines += 5
assert len(msg.splitlines()) == expected_lines
@@ -0,0 +1,73 @@
import pytest
import torch
from invokeai.backend.model_manager.load.optimizations import _no_op, skip_torch_weight_init
@pytest.mark.parametrize(
["torch_module", "layer_args"],
[
(torch.nn.Linear, {"in_features": 10, "out_features": 20}),
(torch.nn.Conv1d, {"in_channels": 10, "out_channels": 20, "kernel_size": 3}),
(torch.nn.Conv2d, {"in_channels": 10, "out_channels": 20, "kernel_size": 3}),
(torch.nn.Conv3d, {"in_channels": 10, "out_channels": 20, "kernel_size": 3}),
(torch.nn.Embedding, {"num_embeddings": 10, "embedding_dim": 10}),
],
)
def test_skip_torch_weight_init_linear(torch_module, layer_args):
"""Test the interactions between `skip_torch_weight_init()` and various torch modules."""
seed = 123
# Initialize a torch layer *before* applying `skip_torch_weight_init()`.
reset_params_fn_before = torch_module.reset_parameters
torch.manual_seed(seed)
layer_before = torch_module(**layer_args)
# Initialize a torch layer while `skip_torch_weight_init()` is applied.
with skip_torch_weight_init():
reset_params_fn_during = torch_module.reset_parameters
torch.manual_seed(123)
layer_during = torch_module(**layer_args)
# Initialize a torch layer *after* applying `skip_torch_weight_init()`.
reset_params_fn_after = torch_module.reset_parameters
torch.manual_seed(123)
layer_after = torch_module(**layer_args)
# Check that reset_parameters is skipped while `skip_torch_weight_init()` is active.
assert reset_params_fn_during == _no_op
assert not torch.allclose(layer_before.weight, layer_during.weight)
if hasattr(layer_before, "bias"):
assert not torch.allclose(layer_before.bias, layer_during.bias)
# Check that the original behavior is restored after `skip_torch_weight_init()` ends.
assert reset_params_fn_before is reset_params_fn_after
assert torch.allclose(layer_before.weight, layer_after.weight)
if hasattr(layer_before, "bias"):
assert torch.allclose(layer_before.bias, layer_after.bias)
def test_skip_torch_weight_init_restores_base_class_behavior():
"""Test that `skip_torch_weight_init()` correctly restores the original behavior of torch.nn.Conv*d modules. This
test was created to catch a previous bug where `reset_parameters` was being copied from the base `_ConvNd` class to
its child classes (like `Conv1d`).
"""
with skip_torch_weight_init():
# There is no need to do anything while the context manager is applied, we're just testing that the original
# behavior is restored correctly.
pass
# Mock the behavior of another library that monkey patches `torch.nn.modules.conv._ConvNd.reset_parameters` and
# expects it to affect all of the sub-classes (e.g. `torch.nn.Conv1D`, `torch.nn.Conv2D`, etc.).
called_monkey_patched_fn = False
def monkey_patched_fn(*args, **kwargs):
nonlocal called_monkey_patched_fn
called_monkey_patched_fn = True
saved_fn = torch.nn.modules.conv._ConvNd.reset_parameters
torch.nn.modules.conv._ConvNd.reset_parameters = monkey_patched_fn
_ = torch.nn.Conv1d(10, 20, 3)
torch.nn.modules.conv._ConvNd.reset_parameters = saved_fn
assert called_monkey_patched_fn
@@ -0,0 +1,405 @@
from pathlib import Path
from typing import List
import pytest
from invokeai.backend.model_manager.taxonomy import ModelRepoVariant
from invokeai.backend.model_manager.util.select_hf_files import filter_files
# This is the full list of model paths returned by the HF API for sdxl-base
@pytest.fixture
def sdxl_base_files() -> List[Path]:
return [
Path(x)
for x in [
".gitattributes",
"01.png",
"LICENSE.md",
"README.md",
"comparison.png",
"model_index.json",
"pipeline.png",
"scheduler/scheduler_config.json",
"sd_xl_base_1.0.safetensors",
"sd_xl_base_1.0_0.9vae.safetensors",
"sd_xl_offset_example-lora_1.0.safetensors",
"text_encoder/config.json",
"text_encoder/flax_model.msgpack",
"text_encoder/model.fp16.safetensors",
"text_encoder/model.onnx",
"text_encoder/model.safetensors",
"text_encoder/openvino_model.bin",
"text_encoder/openvino_model.xml",
"text_encoder_2/config.json",
"text_encoder_2/flax_model.msgpack",
"text_encoder_2/model.fp16.safetensors",
"text_encoder_2/model.onnx",
"text_encoder_2/model.onnx_data",
"text_encoder_2/model.safetensors",
"text_encoder_2/openvino_model.bin",
"text_encoder_2/openvino_model.xml",
"tokenizer/merges.txt",
"tokenizer/special_tokens_map.json",
"tokenizer/tokenizer_config.json",
"tokenizer/vocab.json",
"tokenizer_2/merges.txt",
"tokenizer_2/special_tokens_map.json",
"tokenizer_2/tokenizer_config.json",
"tokenizer_2/vocab.json",
"unet/config.json",
"unet/diffusion_flax_model.msgpack",
"unet/diffusion_pytorch_model.fp16.safetensors",
"unet/diffusion_pytorch_model.safetensors",
"unet/model.onnx",
"unet/model.onnx_data",
"unet/openvino_model.bin",
"unet/openvino_model.xml",
"vae/config.json",
"vae/diffusion_flax_model.msgpack",
"vae/diffusion_pytorch_model.fp16.safetensors",
"vae/diffusion_pytorch_model.safetensors",
"vae_1_0/config.json",
"vae_1_0/diffusion_pytorch_model.fp16.safetensors",
"vae_1_0/diffusion_pytorch_model.safetensors",
"vae_decoder/config.json",
"vae_decoder/model.onnx",
"vae_decoder/openvino_model.bin",
"vae_decoder/openvino_model.xml",
"vae_encoder/config.json",
"vae_encoder/model.onnx",
"vae_encoder/openvino_model.bin",
"vae_encoder/openvino_model.xml",
]
]
# This are what we expect to get when various diffusers variants are requested
@pytest.mark.parametrize(
"variant,expected_list",
[
(
None,
[
"model_index.json",
"scheduler/scheduler_config.json",
"text_encoder/config.json",
"text_encoder/model.safetensors",
"text_encoder_2/config.json",
"text_encoder_2/model.safetensors",
"tokenizer/merges.txt",
"tokenizer/special_tokens_map.json",
"tokenizer/tokenizer_config.json",
"tokenizer/vocab.json",
"tokenizer_2/merges.txt",
"tokenizer_2/special_tokens_map.json",
"tokenizer_2/tokenizer_config.json",
"tokenizer_2/vocab.json",
"unet/config.json",
"unet/diffusion_pytorch_model.safetensors",
"vae/config.json",
"vae/diffusion_pytorch_model.safetensors",
"vae_1_0/config.json",
"vae_1_0/diffusion_pytorch_model.safetensors",
],
),
(
ModelRepoVariant.Default,
[
"model_index.json",
"scheduler/scheduler_config.json",
"text_encoder/config.json",
"text_encoder/model.safetensors",
"text_encoder_2/config.json",
"text_encoder_2/model.safetensors",
"tokenizer/merges.txt",
"tokenizer/special_tokens_map.json",
"tokenizer/tokenizer_config.json",
"tokenizer/vocab.json",
"tokenizer_2/merges.txt",
"tokenizer_2/special_tokens_map.json",
"tokenizer_2/tokenizer_config.json",
"tokenizer_2/vocab.json",
"unet/config.json",
"unet/diffusion_pytorch_model.safetensors",
"vae/config.json",
"vae/diffusion_pytorch_model.safetensors",
"vae_1_0/config.json",
"vae_1_0/diffusion_pytorch_model.safetensors",
],
),
(
ModelRepoVariant.OpenVINO,
[
"model_index.json",
"scheduler/scheduler_config.json",
"text_encoder/config.json",
"text_encoder/openvino_model.bin",
"text_encoder/openvino_model.xml",
"text_encoder_2/config.json",
"text_encoder_2/openvino_model.bin",
"text_encoder_2/openvino_model.xml",
"tokenizer/merges.txt",
"tokenizer/special_tokens_map.json",
"tokenizer/tokenizer_config.json",
"tokenizer/vocab.json",
"tokenizer_2/merges.txt",
"tokenizer_2/special_tokens_map.json",
"tokenizer_2/tokenizer_config.json",
"tokenizer_2/vocab.json",
"unet/config.json",
"unet/openvino_model.bin",
"unet/openvino_model.xml",
"vae_decoder/config.json",
"vae_decoder/openvino_model.bin",
"vae_decoder/openvino_model.xml",
"vae_encoder/config.json",
"vae_encoder/openvino_model.bin",
"vae_encoder/openvino_model.xml",
],
),
(
ModelRepoVariant.FP16,
[
"model_index.json",
"scheduler/scheduler_config.json",
"text_encoder/config.json",
"text_encoder/model.fp16.safetensors",
"text_encoder_2/config.json",
"text_encoder_2/model.fp16.safetensors",
"tokenizer/merges.txt",
"tokenizer/special_tokens_map.json",
"tokenizer/tokenizer_config.json",
"tokenizer/vocab.json",
"tokenizer_2/merges.txt",
"tokenizer_2/special_tokens_map.json",
"tokenizer_2/tokenizer_config.json",
"tokenizer_2/vocab.json",
"unet/config.json",
"unet/diffusion_pytorch_model.fp16.safetensors",
"vae/config.json",
"vae/diffusion_pytorch_model.fp16.safetensors",
"vae_1_0/config.json",
"vae_1_0/diffusion_pytorch_model.fp16.safetensors",
],
),
(
ModelRepoVariant.ONNX,
[
"model_index.json",
"scheduler/scheduler_config.json",
"text_encoder/config.json",
"text_encoder/model.onnx",
"text_encoder_2/config.json",
"text_encoder_2/model.onnx",
"text_encoder_2/model.onnx_data",
"tokenizer/merges.txt",
"tokenizer/special_tokens_map.json",
"tokenizer/tokenizer_config.json",
"tokenizer/vocab.json",
"tokenizer_2/merges.txt",
"tokenizer_2/special_tokens_map.json",
"tokenizer_2/tokenizer_config.json",
"tokenizer_2/vocab.json",
"unet/config.json",
"unet/model.onnx",
"unet/model.onnx_data",
"vae_decoder/config.json",
"vae_decoder/model.onnx",
"vae_encoder/config.json",
"vae_encoder/model.onnx",
],
),
(
ModelRepoVariant.Flax,
[
"model_index.json",
"scheduler/scheduler_config.json",
"text_encoder/config.json",
"text_encoder/flax_model.msgpack",
"text_encoder_2/config.json",
"text_encoder_2/flax_model.msgpack",
"tokenizer/merges.txt",
"tokenizer/special_tokens_map.json",
"tokenizer/tokenizer_config.json",
"tokenizer/vocab.json",
"tokenizer_2/merges.txt",
"tokenizer_2/special_tokens_map.json",
"tokenizer_2/tokenizer_config.json",
"tokenizer_2/vocab.json",
"unet/config.json",
"unet/diffusion_flax_model.msgpack",
"vae/config.json",
"vae/diffusion_flax_model.msgpack",
],
),
],
)
def test_select(sdxl_base_files: List[Path], variant: ModelRepoVariant, expected_list: List[str]) -> None:
print(f"testing variant {variant}")
filtered_files = filter_files(sdxl_base_files, variant)
assert set(filtered_files) == {Path(x) for x in expected_list}
@pytest.fixture
def sd15_test_files() -> list[Path]:
return [
Path(f)
for f in [
"feature_extractor/preprocessor_config.json",
"safety_checker/config.json",
"safety_checker/model.fp16.safetensors",
"safety_checker/model.safetensors",
"safety_checker/pytorch_model.bin",
"safety_checker/pytorch_model.fp16.bin",
"scheduler/scheduler_config.json",
"text_encoder/config.json",
"text_encoder/model.fp16.safetensors",
"text_encoder/model.safetensors",
"text_encoder/pytorch_model.bin",
"text_encoder/pytorch_model.fp16.bin",
"tokenizer/merges.txt",
"tokenizer/special_tokens_map.json",
"tokenizer/tokenizer_config.json",
"tokenizer/vocab.json",
"unet/config.json",
"unet/diffusion_pytorch_model.bin",
"unet/diffusion_pytorch_model.fp16.bin",
"unet/diffusion_pytorch_model.fp16.safetensors",
"unet/diffusion_pytorch_model.non_ema.bin",
"unet/diffusion_pytorch_model.non_ema.safetensors",
"unet/diffusion_pytorch_model.safetensors",
"vae/config.json",
"vae/diffusion_pytorch_model.bin",
"vae/diffusion_pytorch_model.fp16.bin",
"vae/diffusion_pytorch_model.fp16.safetensors",
"vae/diffusion_pytorch_model.safetensors",
]
]
@pytest.mark.parametrize(
"variant,expected_files",
[
(
ModelRepoVariant.FP16,
[
"feature_extractor/preprocessor_config.json",
"safety_checker/config.json",
"safety_checker/model.fp16.safetensors",
"scheduler/scheduler_config.json",
"text_encoder/config.json",
"text_encoder/model.fp16.safetensors",
"tokenizer/merges.txt",
"tokenizer/special_tokens_map.json",
"tokenizer/tokenizer_config.json",
"tokenizer/vocab.json",
"unet/config.json",
"unet/diffusion_pytorch_model.fp16.safetensors",
"vae/config.json",
"vae/diffusion_pytorch_model.fp16.safetensors",
],
),
(
ModelRepoVariant.FP32,
[
"feature_extractor/preprocessor_config.json",
"safety_checker/config.json",
"safety_checker/model.safetensors",
"scheduler/scheduler_config.json",
"text_encoder/config.json",
"text_encoder/model.safetensors",
"tokenizer/merges.txt",
"tokenizer/special_tokens_map.json",
"tokenizer/tokenizer_config.json",
"tokenizer/vocab.json",
"unet/config.json",
"unet/diffusion_pytorch_model.safetensors",
"vae/config.json",
"vae/diffusion_pytorch_model.safetensors",
],
),
],
)
def test_select_multiple_weights(
sd15_test_files: list[Path], variant: ModelRepoVariant, expected_files: list[str]
) -> None:
filtered_files = filter_files(sd15_test_files, variant)
assert set(filtered_files) == {Path(f) for f in expected_files}
@pytest.fixture
def flux_schnell_test_files() -> list[Path]:
return [
Path(f)
for f in [
"FLUX.1-schnell/.gitattributes",
"FLUX.1-schnell/README.md",
"FLUX.1-schnell/ae.safetensors",
"FLUX.1-schnell/flux1-schnell.safetensors",
"FLUX.1-schnell/model_index.json",
"FLUX.1-schnell/scheduler/scheduler_config.json",
"FLUX.1-schnell/schnell_grid.jpeg",
"FLUX.1-schnell/text_encoder/config.json",
"FLUX.1-schnell/text_encoder/model.safetensors",
"FLUX.1-schnell/text_encoder_2/config.json",
"FLUX.1-schnell/text_encoder_2/model-00001-of-00002.safetensors",
"FLUX.1-schnell/text_encoder_2/model-00002-of-00002.safetensors",
"FLUX.1-schnell/text_encoder_2/model.safetensors.index.json",
"FLUX.1-schnell/tokenizer/merges.txt",
"FLUX.1-schnell/tokenizer/special_tokens_map.json",
"FLUX.1-schnell/tokenizer/tokenizer_config.json",
"FLUX.1-schnell/tokenizer/vocab.json",
"FLUX.1-schnell/tokenizer_2/special_tokens_map.json",
"FLUX.1-schnell/tokenizer_2/spiece.model",
"FLUX.1-schnell/tokenizer_2/tokenizer.json",
"FLUX.1-schnell/tokenizer_2/tokenizer_config.json",
"FLUX.1-schnell/transformer/config.json",
"FLUX.1-schnell/transformer/diffusion_pytorch_model-00001-of-00003.safetensors",
"FLUX.1-schnell/transformer/diffusion_pytorch_model-00002-of-00003.safetensors",
"FLUX.1-schnell/transformer/diffusion_pytorch_model-00003-of-00003.safetensors",
"FLUX.1-schnell/transformer/diffusion_pytorch_model.safetensors.index.json",
"FLUX.1-schnell/vae/config.json",
"FLUX.1-schnell/vae/diffusion_pytorch_model.safetensors",
]
]
@pytest.mark.parametrize(
["variant", "expected_files"],
[
(
ModelRepoVariant.Default,
[
"FLUX.1-schnell/model_index.json",
"FLUX.1-schnell/scheduler/scheduler_config.json",
"FLUX.1-schnell/text_encoder/config.json",
"FLUX.1-schnell/text_encoder/model.safetensors",
"FLUX.1-schnell/text_encoder_2/config.json",
"FLUX.1-schnell/text_encoder_2/model-00001-of-00002.safetensors",
"FLUX.1-schnell/text_encoder_2/model-00002-of-00002.safetensors",
"FLUX.1-schnell/text_encoder_2/model.safetensors.index.json",
"FLUX.1-schnell/tokenizer/merges.txt",
"FLUX.1-schnell/tokenizer/special_tokens_map.json",
"FLUX.1-schnell/tokenizer/tokenizer_config.json",
"FLUX.1-schnell/tokenizer/vocab.json",
"FLUX.1-schnell/tokenizer_2/special_tokens_map.json",
"FLUX.1-schnell/tokenizer_2/spiece.model",
"FLUX.1-schnell/tokenizer_2/tokenizer.json",
"FLUX.1-schnell/tokenizer_2/tokenizer_config.json",
"FLUX.1-schnell/transformer/config.json",
"FLUX.1-schnell/transformer/diffusion_pytorch_model-00001-of-00003.safetensors",
"FLUX.1-schnell/transformer/diffusion_pytorch_model-00002-of-00003.safetensors",
"FLUX.1-schnell/transformer/diffusion_pytorch_model-00003-of-00003.safetensors",
"FLUX.1-schnell/transformer/diffusion_pytorch_model.safetensors.index.json",
"FLUX.1-schnell/vae/config.json",
"FLUX.1-schnell/vae/diffusion_pytorch_model.safetensors",
],
),
],
)
def test_select_flux_schnell_files(
flux_schnell_test_files: list[Path], variant: ModelRepoVariant, expected_files: list[str]
) -> None:
filtered_files = filter_files(flux_schnell_test_files, variant)
assert set(filtered_files) == {Path(f) for f in expected_files}
@@ -0,0 +1,25 @@
import torch
from invokeai.backend.patches.layers.flux_control_lora_layer import FluxControlLoRALayer
def test_flux_control_lora_layer_get_parameters():
"""Test getting weight and bias parameters from FluxControlLoRALayer."""
small_in_features = 4
big_in_features = 8
out_features = 16
rank = 4
alpha = 16.0
layer = FluxControlLoRALayer(
up=torch.ones(out_features, rank), mid=None, down=torch.ones(rank, big_in_features), alpha=alpha, bias=None
)
# Create mock original module
orig_module = torch.nn.Linear(small_in_features, out_features)
# Test that get_parameters() behaves as expected in spite of the difference in in_features shapes.
params = layer.get_parameters(dict(orig_module.named_parameters(recurse=False)), weight=1.0)
assert "weight" in params
assert params["weight"].shape == (out_features, big_in_features)
assert params["weight"].allclose(torch.ones(out_features, big_in_features) * alpha)
assert "bias" not in params # No bias in this case
@@ -0,0 +1,114 @@
import logging
import pytest
import torch
from invokeai.backend.patches.layers.lora_layer import LoRALayer
def test_lora_layer_init_from_state_dict():
"""Test initializing a LoRALayer from state dict values."""
# Create mock state dict values
in_features = 8
out_features = 16
rank = 4
alpha = 16.0
values = {
"lora_up.weight": torch.ones(out_features, rank),
"lora_down.weight": torch.ones(rank, in_features),
"alpha": torch.tensor(alpha),
}
layer = LoRALayer.from_state_dict_values(values)
assert layer.up.shape == (out_features, rank)
assert layer.down.shape == (rank, in_features)
assert layer._alpha == alpha
assert layer.bias is None
def test_lora_layer_init_from_state_dict_with_unhandled_keys_logs_warning(caplog: pytest.LogCaptureFixture):
"""Test initializing a LoRALayer from state dict values with an unhandled key."""
in_features = 8
out_features = 16
rank = 4
alpha = 16.0
values = {
"lora_up.weight": torch.ones(out_features, rank),
"lora_down.weight": torch.ones(rank, in_features),
"alpha": torch.tensor(alpha),
"unhandled_key": torch.randn(4, 4),
}
with caplog.at_level(logging.WARNING):
_ = LoRALayer.from_state_dict_values(values)
assert (
"Unexpected keys found in LoRA/LyCORIS layer, model might work incorrectly! Unexpected keys: {'unhandled_key'}"
in caplog.text
)
@pytest.mark.parametrize(
["device"],
[
pytest.param("cuda", marks=pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA device")),
pytest.param(
"mps", marks=pytest.mark.skipif(not torch.backends.mps.is_available(), reason="requires MPS device")
),
],
)
def test_lora_layer_to(device: str):
in_features = 8
out_features = 16
rank = 4
alpha = 16.0
values = {
"lora_up.weight": torch.ones(out_features, rank),
"lora_down.weight": torch.ones(rank, in_features),
"alpha": torch.tensor(alpha),
}
layer = LoRALayer.from_state_dict_values(values)
# Layer is initialized on the CPU.
assert layer.up.device.type == "cpu"
assert layer.down.device.type == "cpu"
# Test moving to device.
layer.to(device=torch.device(device))
assert layer.up.device.type == device
assert layer.down.device.type == device
def test_lora_layer_calc_size():
"""Test calculating memory size of LoRALayer tensors."""
# Initialize weights with random shapes.
up = torch.randn(1, 2)
mid = torch.randn(2, 3)
down = torch.randn(3, 4)
bias = torch.randn(5)
layer = LoRALayer(up=up, mid=mid, down=down, alpha=8.0, bias=bias)
assert layer.calc_size() == sum(tensor.numel() * tensor.element_size() for tensor in [up, mid, down, bias])
def test_lora_layer_get_parameters():
"""Test getting weight and bias parameters from LoRALayer."""
in_features = 8
out_features = 16
rank = 4
alpha = 16.0
values = {
"lora_up.weight": torch.ones(out_features, rank),
"lora_down.weight": torch.ones(rank, in_features),
"alpha": torch.tensor(alpha),
}
layer = LoRALayer.from_state_dict_values(values)
# Create mock original module
orig_module = torch.nn.Linear(in_features, out_features)
params = layer.get_parameters(dict(orig_module.named_parameters(recurse=False)), weight=1.0)
assert "weight" in params
assert params["weight"].shape == orig_module.weight.shape
assert params["weight"].allclose(torch.ones(out_features, in_features) * alpha)
assert "bias" not in params # No bias in this case
@@ -0,0 +1,49 @@
import pytest
import torch
from invokeai.backend.patches.layers.set_parameter_layer import SetParameterLayer
def test_set_parameter_layer_get_parameters():
orig_module = torch.nn.Linear(4, 8)
target_weight = torch.randn(8, 4)
layer = SetParameterLayer(param_name="weight", weight=target_weight)
params = layer.get_parameters(dict(orig_module.named_parameters(recurse=False)), weight=1.0)
assert len(params) == 1
new_weight = orig_module.weight + params["weight"]
assert torch.allclose(new_weight, target_weight)
@pytest.mark.parametrize(
["device"],
[
pytest.param("cuda", marks=pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA device")),
pytest.param(
"mps", marks=pytest.mark.skipif(not torch.backends.mps.is_available(), reason="requires MPS device")
),
],
)
def test_set_parameter_layer_to(device: str):
"""Test moving SetParameterLayer to different device/dtype."""
target_weight = torch.randn(8, 4)
layer = SetParameterLayer(param_name="weight", weight=target_weight)
# SetParameterLayer should be initialized on CPU.
assert layer.weight.device.type == "cpu" # type: ignore
# Move to device.
layer.to(device=torch.device(device))
assert layer.weight.device.type == device # type: ignore
def test_set_parameter_layer_calc_size():
"""Test calculating parameter size of SetParameterLayer"""
param = torch.randn(4, 8)
layer = SetParameterLayer(param_name="weight", weight=param)
# Size should be number of elements * bytes per element
expected_size = param.nelement() * param.element_size()
assert layer.calc_size() == expected_size
@@ -0,0 +1,42 @@
# A sample state dict in the Kohya Anima LoRA format.
# These keys are based on Anima LoRAs targeting the Cosmos Predict2 DiT transformer.
# Keys follow the pattern: lora_unet_blocks_{N}_{component}.{suffix}
state_dict_keys: dict[str, list[int]] = {
# Block 0 - cross attention
"lora_unet_blocks_0_cross_attn_k_proj.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_cross_attn_k_proj.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_cross_attn_k_proj.alpha": [],
"lora_unet_blocks_0_cross_attn_q_proj.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_cross_attn_q_proj.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_cross_attn_q_proj.alpha": [],
"lora_unet_blocks_0_cross_attn_v_proj.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_cross_attn_v_proj.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_cross_attn_v_proj.alpha": [],
"lora_unet_blocks_0_cross_attn_output_proj.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_cross_attn_output_proj.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_cross_attn_output_proj.alpha": [],
# Block 0 - self attention
"lora_unet_blocks_0_self_attn_k_proj.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_self_attn_k_proj.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_self_attn_k_proj.alpha": [],
"lora_unet_blocks_0_self_attn_q_proj.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_self_attn_q_proj.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_self_attn_q_proj.alpha": [],
"lora_unet_blocks_0_self_attn_v_proj.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_self_attn_v_proj.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_self_attn_v_proj.alpha": [],
"lora_unet_blocks_0_self_attn_output_proj.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_self_attn_output_proj.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_self_attn_output_proj.alpha": [],
# Block 0 - MLP
"lora_unet_blocks_0_mlp_layer1.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_mlp_layer1.lora_up.weight": [8192, 8],
"lora_unet_blocks_0_mlp_layer1.alpha": [],
"lora_unet_blocks_0_mlp_layer2.lora_down.weight": [8, 8192],
"lora_unet_blocks_0_mlp_layer2.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_mlp_layer2.alpha": [],
# Block 0 - adaln modulation
"lora_unet_blocks_0_adaln_modulation_cross_attn_1.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_adaln_modulation_cross_attn_1.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_adaln_modulation_cross_attn_1.alpha": [],
}
@@ -0,0 +1,34 @@
# A sample state dict in the Kohya Anima LoRA format with Qwen3 text encoder layers.
# Contains both lora_unet_ (transformer) and lora_te_ (Qwen3 encoder) keys.
state_dict_keys: dict[str, list[int]] = {
# Transformer block 0 - cross attention
"lora_unet_blocks_0_cross_attn_k_proj.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_cross_attn_k_proj.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_cross_attn_k_proj.alpha": [],
"lora_unet_blocks_0_cross_attn_q_proj.lora_down.weight": [8, 2048],
"lora_unet_blocks_0_cross_attn_q_proj.lora_up.weight": [2048, 8],
"lora_unet_blocks_0_cross_attn_q_proj.alpha": [],
# Qwen3 text encoder layer 0 - self attention
"lora_te_layers_0_self_attn_q_proj.lora_down.weight": [8, 1024],
"lora_te_layers_0_self_attn_q_proj.lora_up.weight": [1024, 8],
"lora_te_layers_0_self_attn_q_proj.alpha": [],
"lora_te_layers_0_self_attn_k_proj.lora_down.weight": [8, 1024],
"lora_te_layers_0_self_attn_k_proj.lora_up.weight": [1024, 8],
"lora_te_layers_0_self_attn_k_proj.alpha": [],
"lora_te_layers_0_self_attn_v_proj.lora_down.weight": [8, 1024],
"lora_te_layers_0_self_attn_v_proj.lora_up.weight": [1024, 8],
"lora_te_layers_0_self_attn_v_proj.alpha": [],
"lora_te_layers_0_self_attn_o_proj.lora_down.weight": [8, 1024],
"lora_te_layers_0_self_attn_o_proj.lora_up.weight": [1024, 8],
"lora_te_layers_0_self_attn_o_proj.alpha": [],
# Qwen3 text encoder layer 0 - MLP
"lora_te_layers_0_mlp_gate_proj.lora_down.weight": [8, 1024],
"lora_te_layers_0_mlp_gate_proj.lora_up.weight": [2816, 8],
"lora_te_layers_0_mlp_gate_proj.alpha": [],
"lora_te_layers_0_mlp_down_proj.lora_down.weight": [8, 2816],
"lora_te_layers_0_mlp_down_proj.lora_up.weight": [1024, 8],
"lora_te_layers_0_mlp_down_proj.alpha": [],
"lora_te_layers_0_mlp_up_proj.lora_down.weight": [8, 1024],
"lora_te_layers_0_mlp_up_proj.lora_up.weight": [2816, 8],
"lora_te_layers_0_mlp_up_proj.alpha": [],
}
@@ -0,0 +1,18 @@
# A sample state dict in the LoKR Anima LoRA format (with DoRA).
# Some Anima LoRAs use LoKR weights (lokr_w1/lokr_w2) combined with DoRA (dora_scale).
# The dora_scale should be stripped from LoKR layers during conversion.
state_dict_keys: dict[str, list[int]] = {
# Block 0 - cross attention with LoKR + DoRA
"diffusion_model.blocks.0.cross_attn.k_proj.lokr_w1": [2048, 8],
"diffusion_model.blocks.0.cross_attn.k_proj.lokr_w2": [8, 2048],
"diffusion_model.blocks.0.cross_attn.k_proj.alpha": [],
"diffusion_model.blocks.0.cross_attn.k_proj.dora_scale": [2048],
"diffusion_model.blocks.0.cross_attn.q_proj.lokr_w1": [2048, 8],
"diffusion_model.blocks.0.cross_attn.q_proj.lokr_w2": [8, 2048],
"diffusion_model.blocks.0.cross_attn.q_proj.alpha": [],
"diffusion_model.blocks.0.cross_attn.q_proj.dora_scale": [2048],
# Block 0 - self attention with LoKR (no DoRA)
"diffusion_model.blocks.0.self_attn.k_proj.lokr_w1": [2048, 8],
"diffusion_model.blocks.0.self_attn.k_proj.lokr_w2": [8, 2048],
"diffusion_model.blocks.0.self_attn.k_proj.alpha": [],
}
@@ -0,0 +1,19 @@
# A sample state dict in the diffusers PEFT Anima LoRA format.
# Keys follow the pattern: diffusion_model.blocks.{N}.{component}.lora_{A|B}.weight
state_dict_keys: dict[str, list[int]] = {
# Block 0 - cross attention
"diffusion_model.blocks.0.cross_attn.k_proj.lora_A.weight": [8, 2048],
"diffusion_model.blocks.0.cross_attn.k_proj.lora_B.weight": [2048, 8],
"diffusion_model.blocks.0.cross_attn.q_proj.lora_A.weight": [8, 2048],
"diffusion_model.blocks.0.cross_attn.q_proj.lora_B.weight": [2048, 8],
"diffusion_model.blocks.0.cross_attn.v_proj.lora_A.weight": [8, 2048],
"diffusion_model.blocks.0.cross_attn.v_proj.lora_B.weight": [2048, 8],
# Block 0 - self attention
"diffusion_model.blocks.0.self_attn.k_proj.lora_A.weight": [8, 2048],
"diffusion_model.blocks.0.self_attn.k_proj.lora_B.weight": [2048, 8],
"diffusion_model.blocks.0.self_attn.q_proj.lora_A.weight": [8, 2048],
"diffusion_model.blocks.0.self_attn.q_proj.lora_B.weight": [2048, 8],
# Block 0 - MLP
"diffusion_model.blocks.0.mlp.layer1.lora_A.weight": [8, 2048],
"diffusion_model.blocks.0.mlp.layer1.lora_B.weight": [8192, 8],
}
@@ -0,0 +1,22 @@
# A sample state dict in the BFL LOKR format (FLUX.1 hidden_size=3072).
# These keys represent a LOKR model using BFL internal key names with 'diffusion_model.' prefix.
state_dict_keys = {
"diffusion_model.double_blocks.0.img_attn.proj.lokr_w1": [32, 96],
"diffusion_model.double_blocks.0.img_attn.proj.lokr_w2": [32, 32],
"diffusion_model.double_blocks.0.img_attn.proj.alpha": [],
"diffusion_model.double_blocks.0.img_attn.qkv.lokr_w1": [32, 96],
"diffusion_model.double_blocks.0.img_attn.qkv.lokr_w2": [32, 288],
"diffusion_model.double_blocks.0.img_attn.qkv.alpha": [],
"diffusion_model.double_blocks.0.img_mlp.0.lokr_w1": [32, 96],
"diffusion_model.double_blocks.0.img_mlp.0.lokr_w2": [32, 128],
"diffusion_model.double_blocks.0.img_mlp.0.alpha": [],
"diffusion_model.double_blocks.0.img_mlp.2.lokr_w1": [32, 128],
"diffusion_model.double_blocks.0.img_mlp.2.lokr_w2": [32, 96],
"diffusion_model.double_blocks.0.img_mlp.2.alpha": [],
"diffusion_model.single_blocks.0.linear1.lokr_w1": [32, 128],
"diffusion_model.single_blocks.0.linear1.lokr_w2": [32, 128],
"diffusion_model.single_blocks.0.linear1.alpha": [],
"diffusion_model.single_blocks.0.linear2.lokr_w1": [32, 64],
"diffusion_model.single_blocks.0.linear2.lokr_w2": [32, 48],
"diffusion_model.single_blocks.0.linear2.alpha": [],
}
@@ -0,0 +1,458 @@
state_dict_keys = {
"diffusion_model.double_blocks.0.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.0.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.0.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.0.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.0.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.0.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.0.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.0.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.0.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.0.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.0.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.0.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.0.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.0.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.0.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.0.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.1.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.1.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.1.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.1.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.1.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.1.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.1.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.1.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.1.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.1.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.1.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.1.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.1.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.1.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.1.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.1.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.10.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.10.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.10.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.10.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.10.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.10.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.10.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.10.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.10.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.10.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.10.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.10.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.10.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.10.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.10.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.10.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.11.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.11.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.11.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.11.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.11.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.11.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.11.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.11.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.11.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.11.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.11.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.11.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.11.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.11.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.11.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.11.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.12.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.12.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.12.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.12.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.12.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.12.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.12.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.12.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.12.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.12.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.12.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.12.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.12.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.12.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.12.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.12.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.13.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.13.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.13.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.13.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.13.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.13.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.13.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.13.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.13.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.13.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.13.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.13.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.13.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.13.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.13.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.13.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.14.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.14.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.14.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.14.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.14.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.14.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.14.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.14.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.14.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.14.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.14.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.14.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.14.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.14.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.14.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.14.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.15.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.15.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.15.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.15.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.15.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.15.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.15.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.15.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.15.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.15.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.15.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.15.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.15.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.15.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.15.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.15.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.16.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.16.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.16.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.16.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.16.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.16.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.16.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.16.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.16.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.16.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.16.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.16.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.16.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.16.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.16.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.16.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.17.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.17.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.17.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.17.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.17.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.17.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.17.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.17.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.17.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.17.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.17.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.17.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.17.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.17.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.17.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.17.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.18.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.18.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.18.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.18.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.18.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.18.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.18.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.18.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.18.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.18.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.18.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.18.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.18.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.18.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.18.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.18.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.2.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.2.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.2.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.2.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.2.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.2.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.2.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.2.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.2.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.2.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.2.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.2.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.2.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.2.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.2.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.2.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.3.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.3.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.3.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.3.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.3.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.3.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.3.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.3.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.3.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.3.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.3.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.3.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.3.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.3.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.3.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.3.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.4.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.4.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.4.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.4.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.4.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.4.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.4.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.4.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.4.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.4.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.4.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.4.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.4.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.4.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.4.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.4.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.5.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.5.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.5.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.5.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.5.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.5.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.5.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.5.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.5.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.5.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.5.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.5.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.5.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.5.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.5.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.5.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.6.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.6.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.6.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.6.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.6.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.6.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.6.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.6.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.6.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.6.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.6.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.6.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.6.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.6.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.6.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.6.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.7.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.7.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.7.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.7.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.7.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.7.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.7.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.7.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.7.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.7.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.7.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.7.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.7.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.7.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.7.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.7.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.8.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.8.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.8.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.8.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.8.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.8.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.8.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.8.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.8.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.8.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.8.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.8.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.8.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.8.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.8.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.8.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.9.img_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.9.img_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.9.img_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.9.img_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.9.img_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.9.img_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.9.img_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.9.img_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.9.txt_attn.proj.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.9.txt_attn.proj.lora_B.weight": [3072, 16],
"diffusion_model.double_blocks.9.txt_attn.qkv.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.9.txt_attn.qkv.lora_B.weight": [9216, 16],
"diffusion_model.double_blocks.9.txt_mlp.0.lora_A.weight": [16, 3072],
"diffusion_model.double_blocks.9.txt_mlp.0.lora_B.weight": [12288, 16],
"diffusion_model.double_blocks.9.txt_mlp.2.lora_A.weight": [16, 12288],
"diffusion_model.double_blocks.9.txt_mlp.2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.0.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.0.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.0.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.0.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.1.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.1.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.1.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.1.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.10.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.10.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.10.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.10.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.11.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.11.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.11.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.11.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.12.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.12.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.12.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.12.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.13.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.13.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.13.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.13.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.14.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.14.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.14.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.14.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.15.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.15.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.15.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.15.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.16.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.16.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.16.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.16.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.17.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.17.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.17.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.17.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.18.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.18.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.18.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.18.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.19.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.19.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.19.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.19.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.2.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.2.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.2.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.2.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.20.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.20.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.20.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.20.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.21.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.21.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.21.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.21.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.22.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.22.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.22.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.22.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.23.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.23.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.23.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.23.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.24.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.24.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.24.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.24.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.25.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.25.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.25.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.25.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.26.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.26.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.26.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.26.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.27.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.27.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.27.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.27.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.28.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.28.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.28.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.28.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.29.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.29.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.29.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.29.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.3.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.3.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.3.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.3.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.30.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.30.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.30.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.30.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.31.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.31.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.31.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.31.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.32.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.32.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.32.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.32.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.33.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.33.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.33.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.33.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.34.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.34.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.34.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.34.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.35.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.35.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.35.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.35.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.36.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.36.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.36.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.36.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.37.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.37.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.37.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.37.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.4.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.4.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.4.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.4.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.5.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.5.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.5.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.5.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.6.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.6.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.6.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.6.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.7.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.7.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.7.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.7.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.8.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.8.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.8.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.8.linear2.lora_B.weight": [3072, 16],
"diffusion_model.single_blocks.9.linear1.lora_A.weight": [16, 3072],
"diffusion_model.single_blocks.9.linear1.lora_B.weight": [21504, 16],
"diffusion_model.single_blocks.9.linear2.lora_A.weight": [16, 15360],
"diffusion_model.single_blocks.9.linear2.lora_B.weight": [3072, 16],
}
@@ -0,0 +1,766 @@
# A sample state dict in the Diffusers FLUX LoRA format with base_model.model prefix.
# These keys are based on the LoRA model in peft_adapter_model.safetensors
state_dict_keys = {
"base_model.model.proj_out.lora_A.weight": [4, 3072],
"base_model.model.proj_out.lora_B.weight": [64, 4],
"base_model.model.single_transformer_blocks.0.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.0.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.0.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.0.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.0.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.0.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.0.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.0.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.0.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.0.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.1.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.1.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.1.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.1.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.1.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.1.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.1.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.1.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.1.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.1.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.10.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.10.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.10.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.10.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.10.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.10.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.10.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.10.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.10.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.10.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.11.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.11.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.11.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.11.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.11.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.11.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.11.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.11.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.11.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.11.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.12.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.12.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.12.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.12.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.12.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.12.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.12.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.12.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.12.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.12.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.13.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.13.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.13.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.13.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.13.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.13.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.13.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.13.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.13.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.13.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.14.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.14.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.14.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.14.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.14.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.14.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.14.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.14.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.14.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.14.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.15.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.15.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.15.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.15.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.15.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.15.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.15.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.15.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.15.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.15.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.16.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.16.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.16.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.16.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.16.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.16.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.16.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.16.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.16.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.16.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.17.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.17.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.17.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.17.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.17.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.17.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.17.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.17.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.17.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.17.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.18.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.18.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.18.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.18.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.18.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.18.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.18.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.18.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.18.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.18.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.19.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.19.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.19.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.19.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.19.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.19.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.19.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.19.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.19.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.19.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.2.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.2.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.2.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.2.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.2.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.2.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.2.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.2.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.2.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.2.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.20.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.20.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.20.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.20.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.20.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.20.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.20.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.20.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.20.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.20.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.21.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.21.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.21.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.21.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.21.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.21.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.21.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.21.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.21.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.21.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.22.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.22.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.22.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.22.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.22.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.22.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.22.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.22.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.22.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.22.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.23.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.23.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.23.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.23.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.23.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.23.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.23.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.23.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.23.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.23.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.24.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.24.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.24.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.24.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.24.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.24.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.24.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.24.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.24.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.24.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.25.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.25.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.25.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.25.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.25.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.25.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.25.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.25.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.25.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.25.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.26.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.26.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.26.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.26.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.26.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.26.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.26.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.26.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.26.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.26.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.27.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.27.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.27.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.27.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.27.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.27.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.27.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.27.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.27.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.27.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.28.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.28.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.28.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.28.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.28.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.28.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.28.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.28.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.28.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.28.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.29.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.29.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.29.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.29.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.29.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.29.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.29.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.29.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.29.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.29.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.3.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.3.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.3.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.3.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.3.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.3.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.3.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.3.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.3.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.3.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.30.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.30.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.30.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.30.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.30.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.30.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.30.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.30.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.30.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.30.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.31.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.31.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.31.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.31.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.31.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.31.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.31.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.31.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.31.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.31.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.32.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.32.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.32.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.32.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.32.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.32.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.32.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.32.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.32.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.32.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.33.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.33.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.33.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.33.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.33.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.33.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.33.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.33.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.33.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.33.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.34.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.34.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.34.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.34.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.34.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.34.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.34.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.34.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.34.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.34.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.35.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.35.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.35.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.35.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.35.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.35.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.35.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.35.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.35.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.35.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.36.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.36.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.36.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.36.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.36.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.36.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.36.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.36.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.36.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.36.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.37.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.37.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.37.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.37.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.37.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.37.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.37.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.37.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.37.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.37.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.4.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.4.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.4.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.4.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.4.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.4.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.4.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.4.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.4.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.4.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.5.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.5.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.5.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.5.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.5.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.5.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.5.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.5.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.5.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.5.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.6.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.6.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.6.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.6.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.6.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.6.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.6.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.6.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.6.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.6.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.7.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.7.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.7.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.7.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.7.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.7.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.7.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.7.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.7.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.7.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.8.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.8.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.8.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.8.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.8.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.8.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.8.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.8.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.8.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.8.proj_out.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.9.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.9.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.9.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.9.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.9.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.9.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.single_transformer_blocks.9.proj_mlp.lora_A.weight": [4, 3072],
"base_model.model.single_transformer_blocks.9.proj_mlp.lora_B.weight": [12288, 4],
"base_model.model.single_transformer_blocks.9.proj_out.lora_A.weight": [4, 15360],
"base_model.model.single_transformer_blocks.9.proj_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.0.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.0.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.0.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.0.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.0.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.0.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.0.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.0.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.0.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.0.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.0.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.0.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.0.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.0.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.0.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.0.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.0.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.0.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.0.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.0.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.1.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.1.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.1.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.1.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.1.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.1.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.1.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.1.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.1.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.1.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.1.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.1.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.1.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.1.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.1.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.1.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.1.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.1.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.1.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.1.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.10.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.10.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.10.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.10.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.10.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.10.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.10.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.10.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.10.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.10.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.10.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.10.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.10.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.10.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.10.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.10.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.10.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.10.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.10.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.10.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.11.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.11.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.11.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.11.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.11.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.11.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.11.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.11.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.11.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.11.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.11.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.11.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.11.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.11.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.11.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.11.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.11.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.11.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.11.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.11.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.12.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.12.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.12.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.12.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.12.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.12.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.12.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.12.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.12.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.12.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.12.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.12.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.12.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.12.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.12.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.12.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.12.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.12.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.12.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.12.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.13.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.13.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.13.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.13.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.13.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.13.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.13.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.13.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.13.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.13.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.13.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.13.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.13.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.13.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.13.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.13.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.13.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.13.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.13.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.13.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.14.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.14.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.14.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.14.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.14.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.14.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.14.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.14.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.14.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.14.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.14.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.14.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.14.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.14.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.14.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.14.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.14.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.14.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.14.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.14.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.15.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.15.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.15.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.15.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.15.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.15.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.15.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.15.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.15.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.15.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.15.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.15.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.15.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.15.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.15.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.15.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.15.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.15.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.15.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.15.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.16.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.16.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.16.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.16.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.16.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.16.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.16.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.16.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.16.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.16.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.16.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.16.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.16.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.16.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.16.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.16.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.16.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.16.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.16.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.16.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.17.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.17.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.17.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.17.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.17.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.17.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.17.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.17.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.17.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.17.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.17.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.17.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.17.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.17.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.17.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.17.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.17.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.17.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.17.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.17.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.18.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.18.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.18.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.18.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.18.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.18.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.18.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.18.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.18.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.18.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.18.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.18.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.18.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.18.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.18.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.18.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.18.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.18.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.18.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.18.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.2.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.2.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.2.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.2.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.2.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.2.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.2.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.2.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.2.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.2.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.2.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.2.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.2.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.2.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.2.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.2.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.2.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.2.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.2.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.2.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.3.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.3.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.3.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.3.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.3.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.3.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.3.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.3.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.3.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.3.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.3.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.3.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.3.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.3.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.3.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.3.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.3.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.3.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.3.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.3.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.4.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.4.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.4.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.4.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.4.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.4.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.4.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.4.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.4.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.4.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.4.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.4.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.4.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.4.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.4.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.4.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.4.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.4.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.4.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.4.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.5.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.5.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.5.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.5.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.5.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.5.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.5.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.5.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.5.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.5.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.5.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.5.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.5.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.5.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.5.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.5.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.5.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.5.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.5.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.5.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.6.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.6.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.6.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.6.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.6.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.6.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.6.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.6.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.6.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.6.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.6.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.6.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.6.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.6.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.6.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.6.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.6.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.6.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.6.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.6.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.7.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.7.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.7.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.7.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.7.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.7.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.7.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.7.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.7.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.7.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.7.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.7.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.7.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.7.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.7.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.7.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.7.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.7.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.7.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.7.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.8.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.8.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.8.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.8.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.8.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.8.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.8.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.8.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.8.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.8.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.8.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.8.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.8.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.8.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.8.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.8.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.8.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.8.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.8.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.8.ff_context.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.9.attn.add_k_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.9.attn.add_k_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.9.attn.add_q_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.9.attn.add_q_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.9.attn.add_v_proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.9.attn.add_v_proj.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.9.attn.to_add_out.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.9.attn.to_add_out.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.9.attn.to_k.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.9.attn.to_k.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.9.attn.to_out.0.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.9.attn.to_out.0.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.9.attn.to_q.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.9.attn.to_q.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.9.attn.to_v.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.9.attn.to_v.lora_B.weight": [3072, 4],
"base_model.model.transformer_blocks.9.ff.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.9.ff.net.0.proj.lora_B.weight": [12288, 4],
"base_model.model.transformer_blocks.9.ff_context.net.0.proj.lora_A.weight": [4, 3072],
"base_model.model.transformer_blocks.9.ff_context.net.0.proj.lora_B.weight": [12288, 4],
}

Some files were not shown because too many files have changed in this diff Show More