feat(lora): support the lycoris_ prefix in every native adapter arch

The lycoris_ save format is arch-independent: LyCORIS standalone wraps
the loaded diffusers model and emits the wrapped module path with dots
as underscores, so verbatim passthrough is correct for any arch. Only
flux2 handled it; zimage, chroma, ernie and krea2 reported such files
as not loaded.

- add lycoris_ to KNOWN_PREFIXES_DEFAULT and PASSTHROUGH_PREFIXES_DEFAULT
- drop flux2's per-arch prefix append and resolve_targets branch
- add lycoris_ to ANIMA_PREFIXES (anima replaces the default tuple);
  network_prefix_for already routes it to the transformer namespace
- cover the passthrough with a zimage loader test
This commit is contained in:
CalamitousFelicitousness
2026-07-12 19:58:17 +01:00
parent 0c5961f605
commit 1587b9682d
5 changed files with 53 additions and 20 deletions
+8 -4
View File
@@ -48,7 +48,11 @@ from modules.lora import lora_common as l
# vendor-specific naming conventions. ``lora_transformer_`` is not a vendor
# format but sdnext's own internal transformer namespace; files already saved
# in it (e.g. OneTrainer) pass through verbatim, see :func:`resolve_group_targets`.
KNOWN_PREFIXES_DEFAULT = ("diffusion_model.", "transformer.", "lora_unet_", "lora_transformer_")
# ``lycoris_`` is the LyCORIS-standalone save format: the wrapped diffusers
# module path with dots rendered as underscores. It is arch-independent by
# construction (the wrapped layout equals the target layout), so it lives here
# rather than in any arch's prefix list.
KNOWN_PREFIXES_DEFAULT = ("diffusion_model.", "transformer.", "lora_unet_", "lora_transformer_", "lycoris_")
# Sentinel ``prefix_used`` value emitted by :func:`parse_key` when a bare path
@@ -67,9 +71,9 @@ NETWORK_PREFIX_DEFAULT = "lora_transformer_"
# Prefixes whose parsed ``base`` is already a network-key tail (``arch_prefix +
# base.replace(".", "_")`` matches the stamped module name), so the loader binds
# them directly with no per-arch rewrite. Arch-local already-resolved prefixes
# (e.g. flux2's ``lycoris_``) stay in that arch's ``resolve_targets``.
PASSTHROUGH_PREFIXES_DEFAULT = ("transformer.", BARE_DIFFUSERS_PREFIX_USED, "lora_transformer_")
# them directly with no per-arch rewrite. ``lycoris_`` bases are already
# underscored, so the loader's ``.replace(".", "_")`` is a no-op on them.
PASSTHROUGH_PREFIXES_DEFAULT = ("transformer.", BARE_DIFFUSERS_PREFIX_USED, "lora_transformer_", "lycoris_")
def _resolve_prefix(network_prefix, prefix_used):
+4
View File
@@ -51,6 +51,10 @@ ANIMA_PREFIXES = (
"diffusion_model.",
"lora_te_",
"lora_unet_",
# LyCORIS-standalone save format; resolved by the universal passthrough in
# native_adapter.resolve_group_targets and routed to lora_transformer_ by
# network_prefix_for's default arm.
"lycoris_",
)
+5 -14
View File
@@ -56,7 +56,7 @@ from modules.lora.native_adapter import ChunkSpec
# === Arch-specific prefix configuration ===
KNOWN_PREFIXES = native_adapter.KNOWN_PREFIXES_DEFAULT + ("lycoris_",)
KNOWN_PREFIXES = native_adapter.KNOWN_PREFIXES_DEFAULT
BARE_FLUX_PREFIXES = (
"single_blocks.", "double_blocks.", "img_in.", "txt_in.",
@@ -183,25 +183,16 @@ def resolve_targets(prefix_used, base):
"""Return ``[(diffusers_path, ChunkSpec | None), ...]`` for a parsed group key.
For ``lora_unet_`` prefix, applies ``KOHYA_SUFFIX_MAP`` then ``F2_*_MAP``.
For BFL / bare-BFL, applies ``F2_*_MAP`` directly. ``lycoris_`` is an
already-underscored diffusers path, returned verbatim. Unrecognized
prefixes return an empty list.
For BFL / bare-BFL, applies ``F2_*_MAP`` directly. Unrecognized prefixes
return an empty list.
Universal passthrough prefixes are handled upstream by
:func:`native_adapter.resolve_group_targets`.
Universal passthrough prefixes (including ``lycoris_``) are handled
upstream by :func:`native_adapter.resolve_group_targets`.
"""
if prefix_used == "lora_unet_":
return _kohya_to_diffusers_targets(base)
if prefix_used in (None, "diffusion_model."):
return _bfl_to_diffusers_targets(base)
if prefix_used == "lycoris_":
# base is an already-underscored diffusers path (e.g.
# 'transformer_blocks_0_attn_add_k_proj'). The caller's network_key
# construction does base.replace('.', '_'); for already-underscored
# paths that's a no-op, so the network_key matches the entry stamped
# by lora_convert.assign_network_names_to_compvis_modules
# (e.g. 'lora_transformer_transformer_blocks_0_attn_add_k_proj').
return [(base, None)]
return []
+3 -2
View File
@@ -718,8 +718,9 @@ def test_parse_key_lycoris_prefix():
got = F.parse_key(key, suffixes)
assert got == expected, f'parse_key({key!r}) = {got}, expected {expected}'
# resolve_targets: the underscored path is returned verbatim (no chunk).
targets = F.resolve_targets('lycoris_', 'transformer_blocks_0_attn_add_k_proj')
# Resolution: lycoris_ is a universal passthrough handled upstream of the
# arch resolver, so the underscored path is returned verbatim (no chunk).
targets = F.native_adapter.resolve_group_targets(F.resolve_targets, 'lycoris_', 'transformer_blocks_0_attn_add_k_proj')
assert targets == [('transformer_blocks_0_attn_add_k_proj', None)], f'targets={targets}'
return True
+33
View File
@@ -313,6 +313,24 @@ def sd_lokr_legacy_fused_qkv():
}
def sd_lokr_lycoris_style():
"""LyCORIS-standalone LoKR (``lycoris_`` + underscored diffusers path).
The prefix is resolved by the universal passthrough in native_adapter, not
by anything zimage-specific. ``to_out.0`` exercises the verbatim
round-trip where a naive underscore-to-dot expansion would corrupt the
ModuleList index.
"""
return {
'lycoris_layers_0_attention_to_q.lokr_w1': torch.randn(LOKR_W1_DIM, LOKR_W1_DIM),
'lycoris_layers_0_attention_to_q.lokr_w2': torch.randn(HIDDEN // LOKR_W1_DIM, HIDDEN // LOKR_W1_DIM),
'lycoris_layers_0_attention_to_q.alpha': torch.tensor(float(LOKR_W1_DIM)),
'lycoris_layers_1_attention_to_out_0.lokr_w1': torch.randn(LOKR_W1_DIM, LOKR_W1_DIM),
'lycoris_layers_1_attention_to_out_0.lokr_w2': torch.randn(HIDDEN // LOKR_W1_DIM, HIDDEN // LOKR_W1_DIM),
'lycoris_layers_1_attention_to_out_0.alpha': torch.tensor(float(LOKR_W1_DIM)),
}
def sd_loha_bfl_proj():
"""LoHA on attention.to_out.0 (non-fused; LoHA on fused qkv is skipped by the loader)."""
return {
@@ -599,6 +617,20 @@ def test_lokr_legacy_fused_qkv_chunked():
return True
def test_lokr_lycoris_prefix_passthrough():
"""lycoris_ keys load via the universal passthrough (hoisted, not zimage-specific)."""
net = _load_via(Z.try_load_lokr, sd_lokr_lycoris_style())
assert net is not None and len(net.modules) == 2, f'got {net.modules if net else None}'
expected = {
'lora_transformer_layers_0_attention_to_q',
'lora_transformer_layers_1_attention_to_out_0',
}
assert set(net.modules) == expected, f'got {set(net.modules)}'
for mod in net.modules.values():
assert isinstance(mod, network_lokr.NetworkModuleLokr)
return True
def test_loha_bfl_proj():
"""BFL LoHA on a non-fused proj target binds via NetworkModuleHada."""
net = _load_via(Z.try_load_loha, sd_loha_bfl_proj())
@@ -729,6 +761,7 @@ def run_tests():
test_lora_dora_threading,
test_lokr_bfl_adaln,
test_lokr_legacy_fused_qkv_chunked,
test_lokr_lycoris_prefix_passthrough,
test_loha_bfl_proj,
test_loha_legacy_fused_qkv_chunked,
test_oft_lycoris_no_npe,