Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 13 additions & 3 deletions src/diffusers/loaders/lora_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,11 +669,21 @@ def unfuse_lora(self, components: list[str] | None = None, **kwargs):
if issubclass(model.__class__, (ModelMixin, PreTrainedModel)):
for module in model.modules():
if isinstance(module, BaseTunerLayer):
for adapter in set(module.merged_adapters):
if adapter and adapter in self._merged_adapters:
self._merged_adapters = self._merged_adapters - {adapter}
module.unmerge()

# Only remove an adapter from _merged_adapters once it is no longer
# physically merged in any remaining loadable component. Removing it

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO the second sentence is not needed.

# on the first unfused component would desync the set when the adapter
# is still fused into other components.
remaining_merged: set[str] = set()
for component_name in self._lora_loadable_modules:
component_model = getattr(self, component_name, None)
if component_model is not None and issubclass(component_model.__class__, (ModelMixin, PreTrainedModel)):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why check issubclass instead of isinstance? I also think isinstance(component_model, nn.Module) would be sufficient here, but I'll leave that to the maintainers to decide.

for module in component_model.modules():
if isinstance(module, BaseTunerLayer):
remaining_merged.update(module.merged_adapters)
self._merged_adapters = self._merged_adapters & remaining_merged

def set_adapters(
self,
adapter_names: list[str] | str,
Expand Down
50 changes: 50 additions & 0 deletions tests/lora/test_lora_loader_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,53 @@ def test_local_directory_with_multiple_files_warns_and_uses_first(tmp_path, monk

assert weight_name == first_path.name
assert "contains more than one weights file" in caplog.text


def test_unfuse_lora_partial_components_keeps_merged_adapters_in_sync():
"""Regression test for gh-14214.

Unfusing only a subset of components must keep _merged_adapters in sync
with the adapters still physically fused in the remaining components.
"""
import torch.nn as nn

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why local imports? Only PEFT needs to be local or guarded behind is_peft_available().

from peft import LoraConfig
from peft.tuners.tuners_utils import BaseTunerLayer
from diffusers.loaders.lora_base import LoraBaseMixin
from diffusers.loaders.peft import PeftAdapterMixin
from diffusers.models.modeling_utils import ModelMixin
from diffusers.configuration_utils import ConfigMixin

class TinyModel(ModelMixin, ConfigMixin, PeftAdapterMixin):
config_name = "config.json"
def __init__(self):
super().__init__()
self.linear = nn.Linear(8, 8)

class FakePipeline(LoraBaseMixin):
_lora_loadable_modules = ["unet", "text_encoder"]
def __init__(self, unet, text_encoder):
self._merged_adapters = set()
self.unet, self.text_encoder = unet, text_encoder

unet = TinyModel()
text_encoder = TinyModel()
config = LoraConfig(r=4, lora_alpha=4, target_modules=["linear"], init_lora_weights=False)
unet.add_adapter(config, adapter_name="adapter")
text_encoder.add_adapter(config, adapter_name="adapter")

pipe = FakePipeline(unet, text_encoder)
pipe.fuse_lora(components=["unet", "text_encoder"], adapter_names=["adapter"])
assert pipe.num_fused_loras == 1

pipe.unfuse_lora(components=["text_encoder"])
assert "adapter" in pipe.fused_loras, "adapter should remain tracked while unet is still fused"
assert pipe.num_fused_loras == 1

unet_still_merged = any(
isinstance(m, BaseTunerLayer) and len(m.merged_adapters) > 0
for m in unet.modules()
)
assert unet_still_merged, "unet should still be physically merged at the PEFT level"

pipe.unfuse_lora(components=["unet"])
assert pipe.num_fused_loras == 0
1 change: 1 addition & 0 deletions tests/lora/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2548,3 +2548,4 @@ def test_lora_group_offloading_delete_adapters(self):
# Clean up the hooks to prevent state leak
if hasattr(denoiser, "_diffusers_hook"):
denoiser._diffusers_hook.remove_hook(_GROUP_OFFLOADING, recurse=True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove unrelated changes.

Loading