Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions invokeai/app/invocations/wan_model_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,30 @@ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput:

if getattr(low_config, "variant", None) != main_variant:
raise ValueError("The high-noise and low-noise GGUF models must use the same Wan variant.")
if {primary_expert, low_expert} != {"high", "low"}:
raise ValueError("A Wan A14B GGUF expert pair must contain one high and one low expert.")

# The expert tag is a filename heuristic, so 'none' (untagged)
# is common on community finetunes. The wiring itself is
# explicit user intent — main slot = high, low-noise slot =
# low — so an untagged file is taken at its wired position (or
# inferred as the complement of its tagged partner). Only a
# genuine conflict, both files claiming the *same* expert, is
# an error.
if primary_expert == low_expert != "none":
raise ValueError(
f"Both selected GGUF models are tagged as the {primary_expert}-noise expert. "
"A Wan A14B expert pair must contain one high and one low expert."
)
if primary_expert == "none" and low_expert == "none":
context.logger.warning(
"Neither Wan A14B GGUF filename identifies its expert, so 'Transformer' is assumed to "
"be the high-noise expert and 'Transformer (Low Noise)' the low-noise expert. If the "
"output looks wrong, swap the two models."
)

# Make sure 'transformer' is the high-noise expert and
# 'transformer_low_noise' is the low-noise expert. If the user
# accidentally swapped them, swap back.
if primary_expert == "low" and low_expert == "high":
if primary_expert == "low" or low_expert == "high":
transformer = low_id
transformer_low_noise = primary_id
else:
Expand Down
30 changes: 24 additions & 6 deletions invokeai/backend/model_manager/configs/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1852,18 +1852,36 @@ def _detect_wan_gguf_variant(state_dict: dict[str | int, Any]) -> WanVariantType
return None


# Splits a filename stem into words on both separator and camelCase boundaries:
# "DasiwaWAN22I2V14B_q5High" -> Dasiwa, WAN, 22, I, 2, V, 14, B, q, 5, High
_NAME_WORD_RE = re.compile(r"[A-Z]+(?![a-z])|[A-Z][a-z]*|[a-z]+|\d+")


def _detect_wan_gguf_expert(filename: str) -> Literal["high", "low", "none"]:
"""Filename heuristic for the A14B dual-expert MoE.

Community releases tag each expert in the filename — typically
``high_noise`` / ``low_noise`` (or hyphenated/concatenated variants).
Returns 'none' when neither marker is present (single-expert model or
ambiguous filename).
Community releases tag each expert in the filename. The canonical form is
``high_noise`` / ``low_noise`` (or hyphenated/concatenated variants), but
many finetunes only carry a bare ``high`` / ``low`` marker, e.g.
``SomeFinetune_q5High.gguf``. The bare marker is matched as a whole word —
separator *or* camelCase delimited — so that names containing ``flow``,
``slowmo`` or ``highres`` don't produce a false positive.

Returns 'none' when no marker is present (single-expert model) or when
markers for both experts are present (ambiguous).
"""
name = filename.lower()
if any(s in name for s in ("high_noise", "high-noise", "highnoise")):
is_high = any(s in name for s in ("high_noise", "high-noise", "highnoise"))
is_low = any(s in name for s in ("low_noise", "low-noise", "lownoise"))

if not is_high and not is_low:
words = {word.lower() for word in _NAME_WORD_RE.findall(filename)}
is_high = "high" in words
is_low = "low" in words

if is_high and not is_low:
return "high"
if any(s in name for s in ("low_noise", "low-noise", "lownoise")):
if is_low and not is_high:
return "low"
return "none"

Expand Down
31 changes: 29 additions & 2 deletions tests/app/invocations/test_wan_model_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ def test_gguf_loader_accepts_valid_expert_pair_in_either_order(
_config("low", WanVariantType.T2V_A14B, "high"),
),
(
_config("main", WanVariantType.T2V_A14B, "high"),
_config("low", WanVariantType.T2V_A14B, "none"),
_config("main", WanVariantType.T2V_A14B, "low"),
_config("low", WanVariantType.T2V_A14B, "low"),
),
],
)
Expand All @@ -118,6 +118,33 @@ def test_gguf_loader_rejects_invalid_expert_pair(main_config: SimpleNamespace, l
_invoke(main_config, low_config)


@pytest.mark.parametrize(
"main_expert,low_expert,expected_high_key",
[
# The expert tag comes from a filename heuristic, so untagged community
# finetunes are common. The wiring is explicit intent: take the untagged
# file at its wired position, or as the complement of a tagged partner.
("none", "none", "main"),
("high", "none", "main"),
("none", "low", "main"),
("none", "high", "low"),
("low", "none", "low"),
],
)
def test_gguf_loader_falls_back_to_wiring_for_untagged_experts(
main_expert: str, low_expert: str, expected_high_key: str
) -> None:
output = _invoke(
_config("main", WanVariantType.I2V_A14B, main_expert),
_config("low", WanVariantType.I2V_A14B, low_expert),
)

expected_low_key = "low" if expected_high_key == "main" else "main"
assert output.transformer.transformer.key == expected_high_key
assert output.transformer.transformer_low_noise is not None
assert output.transformer.transformer_low_noise.key == expected_low_key


@pytest.mark.parametrize("low_variant", [WanVariantType.TI2V_5B, WanVariantType.T2V_A14B])
def test_ti2v_5b_main_ignores_wired_low_noise_model(low_variant: WanVariantType) -> None:
"""The field docs promise 'Transformer (Low Noise)' is ignored for the single-expert
Expand Down
13 changes: 13 additions & 0 deletions tests/backend/model_manager/configs/test_wan_gguf_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,19 @@ class TestExpertFilenameHeuristic:
("Wan2.2-A14B-LowNoise-Q4", "low"),
("wan2.2-ti2v-5b-Q4_K_M", "none"),
("wan-A14B-flagship", "none"),
# Community finetunes often tag the expert with a bare marker
# rather than the canonical '<x>_noise' form.
("DasiwaWAN22I2V14BTastysinV8_q5High", "high"),
("DasiwaWAN22I2V14BTastysinV8_q5Low", "low"),
("wan2.2-t2v-a14b-HIGH-Q5_K_M", "high"),
("wan2.2_t2v_a14b_low_q5", "low"),
# ...but the bare marker must be a whole word, or names that merely
# contain 'high'/'low' as a substring get mistagged.
("wan2.2-a14b-slowmotion-lora-merge-q4", "none"),
("Wan2.2-A14B-Flowstate-Q4", "none"),
("wan2.2-a14b-highres-fix-q4", "none"),
# Both markers present -> ambiguous, no guess.
("wan2.2-a14b-high-low-merged-q4", "none"),
],
)
def test_filename_heuristic(self, name: str, expected: str):
Expand Down
Loading