diff --git a/modules/lora/native_adapter.py b/modules/lora/native_adapter.py index 91b6e2d2b..72a330aa4 100644 --- a/modules/lora/native_adapter.py +++ b/modules/lora/native_adapter.py @@ -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): diff --git a/pipelines/anima/anima_lora.py b/pipelines/anima/anima_lora.py index cc9c01dbf..cea98faa8 100644 --- a/pipelines/anima/anima_lora.py +++ b/pipelines/anima/anima_lora.py @@ -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_", ) diff --git a/pipelines/flux/flux2_lora.py b/pipelines/flux/flux2_lora.py index 9754e71b3..681c65087 100644 --- a/pipelines/flux/flux2_lora.py +++ b/pipelines/flux/flux2_lora.py @@ -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 [] diff --git a/test/test-flux2-native-adapters.py b/test/test-flux2-native-adapters.py index a595f529d..162774d42 100644 --- a/test/test-flux2-native-adapters.py +++ b/test/test-flux2-native-adapters.py @@ -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 diff --git a/test/test-zimage-native-adapters.py b/test/test-zimage-native-adapters.py index 6f958161b..9e7a29a0e 100644 --- a/test/test-zimage-native-adapters.py +++ b/test/test-zimage-native-adapters.py @@ -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,