From 4747ca2fc026ff2b8d7f290e92969c1523e9de50 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 9 May 2026 09:38:52 +0200 Subject: [PATCH] lora change detection improvements Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 8 +++++--- modules/extra_networks.py | 9 +++++++++ modules/face/faceid.py | 2 +- modules/framepack/framepack_wrappers.py | 2 +- modules/lora/extra_networks_lora.py | 25 +++++++++++++------------ modules/postprocess/yolo.py | 12 ++++++++---- modules/processing_class.py | 3 ++- 7 files changed, 39 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40dc4aad5..61a36663e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Change Log for SD.Next -## Update for 2026-05-08 +## Update for 2026-05-09 -### Highlights for 2026-05-08 +### Highlights for 2026-05-09 *What's New?* - Image editing models now can work with multiple image inputs! @@ -13,7 +13,7 @@ For full details, see [ChangeLog](https://github.com/vladmandic/automatic/blob/m [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) | [Sponsor](https://github.com/sponsors/vladmandic) -### Details for 2026-05-08 +### Details for 2026-05-09 - **Features** - **Multi-image** workflows! @@ -84,6 +84,8 @@ For full details, see [ChangeLog](https://github.com/vladmandic/automatic/blob/m - cache network thumbnails - `scripts` corrupting control ui state - avoid `callback` duplicate registrations + - pipeline task change causing loss of info on loaded `lora` + - `detailer` handle `lora` internally ## Update for 2026-04-28 diff --git a/modules/extra_networks.py b/modules/extra_networks.py index 67e334a90..c281c521f 100644 --- a/modules/extra_networks.py +++ b/modules/extra_networks.py @@ -37,6 +37,15 @@ class ExtraNetworkParams: else: self.positional.append(item) + def __eq__(self, other): + if not isinstance(other, ExtraNetworkParams): + return False + same = self.positional == other.positional and self.named == other.named + return same + + def __repr__(self): + return f"NetworkParams(positional={self.positional} named={self.named})" + class ExtraNetwork: def __init__(self, name): diff --git a/modules/face/faceid.py b/modules/face/faceid.py index f3cd6f783..b2263d840 100644 --- a/modules/face/faceid.py +++ b/modules/face/faceid.py @@ -214,7 +214,7 @@ def face_id( p.negative_prompts = p.all_negative_prompts[n * p.batch_size:(n+1) * p.batch_size] p.seeds = p.all_seeds[n * p.batch_size:(n+1) * p.batch_size] p.subseeds = p.all_subseeds[n * p.batch_size:(n+1) * p.batch_size] - p.prompts, p.network_data = extra_networks.parse_prompts(p.prompts) + p.prompts, p.network_data = extra_networks.parse_prompts(p.prompts, p.network_data) extra_networks.activate(p, p.network_data) ip_model_dict.update({ diff --git a/modules/framepack/framepack_wrappers.py b/modules/framepack/framepack_wrappers.py index bdff734ae..b9a5e5592 100644 --- a/modules/framepack/framepack_wrappers.py +++ b/modules/framepack/framepack_wrappers.py @@ -70,7 +70,7 @@ def prepare_prompts(p, init_image, prompt:str, section_prompt:str, num_sections: p.prompt = shared.prompt_styles.apply_styles_to_prompt(p.prompt, p.styles) p.negative_prompt = shared.prompt_styles.apply_negative_styles_to_prompt(p.negative_prompt, p.styles) shared.prompt_styles.apply_styles_to_extra(p) - p.prompts, p.network_data = extra_networks.parse_prompts([p.prompt]) + p.prompts, p.network_data = extra_networks.parse_prompts([p.prompt], p.network_data) extra_networks.activate(p) prompt = p.prompts[0] generated_prompts = [''] * num_sections diff --git a/modules/lora/extra_networks_lora.py b/modules/lora/extra_networks_lora.py index 364244b1a..f3f792792 100644 --- a/modules/lora/extra_networks_lora.py +++ b/modules/lora/extra_networks_lora.py @@ -177,11 +177,11 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork): def changed(self, requested: list[str], include: list[str] | None = None, exclude: list[str] | None = None) -> bool: if shared.opts.lora_force_reload: - debug_log(f'Network check: type=LoRA requested={requested} status=forced') - return True + debug_log(f'Network check: type=LoRA requested={requested} status="forced"') + return True, "forced" sd_model = shared.sd_model.pipe if hasattr(shared.sd_model, 'pipe') else shared.sd_model if sd_model is None: - return False + return False, "no model" if not hasattr(sd_model, 'loaded_loras'): sd_model.loaded_loras = {} if include is None or len(include) == 0: @@ -192,15 +192,15 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork): loaded = sd_model.loaded_loras.get(key, []) if len(requested) != len(loaded): sd_model.loaded_loras[key] = requested - debug_log(f'Network check: type=LoRA key="{key}" requested={requested} loaded={loaded} status=changed') - return True + debug_log(f'Network check: type=LoRA key="{key}" requested={requested} loaded={loaded} status="num changed"') + return True, "num changed" for req, load in zip(requested, loaded, strict=False): if req != load: sd_model.loaded_loras[key] = requested - debug_log(f'Network check: type=LoRA key="{key}" requested={requested} loaded={loaded} status=changed') - return True - debug_log(f'Network check: type=LoRA key="{key}" requested={requested} loaded={loaded} status=same') - return False + debug_log(f'Network check: type=LoRA key="{key}" requested={requested} loaded={loaded} status="content changed"') + return True, "content changed" + debug_log(f'Network check: type=LoRA key="{key}" requested={requested} loaded={loaded} status="same"') + return False, "none" def activate(self, p, params_list, step=0, include=None, exclude=None): if exclude is None: @@ -216,6 +216,7 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork): self.model = shared.opts.sd_model_checkpoint names, te_multipliers, unet_multipliers, dyn_dims, lora_modules = parse(p, params_list, step) requested = self.signature(names, te_multipliers, unet_multipliers) + reason = '' load_method = lora_overrides.get_method() if debug: @@ -224,7 +225,7 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork): debug_log(f'Network load: type=LoRA include={include} exclude={exclude} method={load_method} requested={requested} fn={fn}') if load_method == 'diffusers': - has_changed = self.changed(requested) + has_changed, reason = self.changed(requested) if has_changed: jobid = shared.state.begin('LoRA') lora_load.network_load(names, te_multipliers, unet_multipliers, dyn_dims, lora_modules) # load only on first call @@ -237,7 +238,7 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork): else: # native lora_load.network_load(names, te_multipliers, unet_multipliers, dyn_dims) # load - has_changed = self.changed(requested, include, exclude) + has_changed, reason = self.changed(requested, include, exclude) if has_changed: jobid = shared.state.begin('LoRA') if len(l.previously_loaded_networks) > 0: @@ -254,7 +255,7 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork): prompt(p) if has_changed and len(include) == 0: # print only once actual_method = 'native' if any(len(n.modules) > 0 for n in l.loaded_networks) else load_method - log.info(f'Network load: type=LoRA networks={[n.name for n in l.loaded_networks]} method={actual_method} mode={"fuse" if shared.opts.lora_fuse_native else "backup"} te={te_multipliers} unet={unet_multipliers} time={l.timer.summary}') + log.info(f'Network load: type=LoRA networks={[n.name for n in l.loaded_networks]} load={load_method} method={actual_method} mode={"fuse" if shared.opts.lora_fuse_native else "backup"} te={te_multipliers} unet={unet_multipliers} time={l.timer.summary} reason={reason}') def deactivate(self, p, force=False): if len(lora_diffusers.diffuser_loaded) > 0 and (shared.opts.lora_force_reload or force): diff --git a/modules/postprocess/yolo.py b/modules/postprocess/yolo.py index d45650ccf..b89ec5aac 100644 --- a/modules/postprocess/yolo.py +++ b/modules/postprocess/yolo.py @@ -427,9 +427,12 @@ class YoloRestorer(Detailer): pc.negative_prompt = negative_lines[i*len(items)+j] pc.prompts = [pc.prompt] pc.negative_prompts = [pc.negative_prompt] - pc.prompts, pc.network_data = extra_networks.parse_prompts(pc.prompts) - extra_networks.activate(pc, pc.network_data) - log.debug(f'Detail: model="{i+1}:{name}" item={j+1}/{len(items)} box={item.box} label="{item.label}" score={item.score:.2f} seg={detailer_opt(p, "detailer_segmentation")} prompt="{pc.prompt}"') + pc.prompts, pc.network_data = extra_networks.parse_prompts(pc.prompts, pc.network_data) + pc.disable_extra_networks = True # disable processing_diffusers from handling network activation since its handled here + network_same = len(p.network_data.values()) == len(pc.network_data.values()) and all(x == y for x, y in zip(p.network_data.values(), pc.network_data.values())) + if not network_same: + extra_networks.activate(pc, pc.network_data) + log.debug(f'Detail: model="{i+1}:{name}" item={j+1}/{len(items)} box={item.box} label="{item.label}" score={item.score:.2f} seg={detailer_opt(p, "detailer_segmentation")} network={network_same} prompt="{pc.prompt}"') pc.init_images = [image] pc.image_mask = [item.mask] pc.overlay_images = [] @@ -443,7 +446,8 @@ class YoloRestorer(Detailer): # process jobid = shared.state.begin('Detailer') pp = processing.process_images_inner(pc) - extra_networks.deactivate(pc, force=True) + if not network_same: + extra_networks.deactivate(pc, force=True) shared.sd_model.fail_on_switch_error = False shared.state.end(jobid) diff --git a/modules/processing_class.py b/modules/processing_class.py index ecbe47a92..581914204 100644 --- a/modules/processing_class.py +++ b/modules/processing_class.py @@ -279,6 +279,7 @@ class StableDiffusionProcessing: skip_processing: bool = False, override_settings_restore_afterwards: bool = True, override_settings: dict[str, Any] | None = None, + network_data: dict | None = None, # metadata # extra_generation_params: Dict[Any, Any] = {}, # task_args: Dict[str, Any] = {}, @@ -322,7 +323,7 @@ class StableDiffusionProcessing: self.negative_prompt_attention_masks = [] self.disable_extra_networks = False self.iteration = 0 - self.network_data = {} + self.network_data = network_data or {} # initializers self.prompt = prompt