From 0dc38822d5667a282703649071df1ba9675fc98d Mon Sep 17 00:00:00 2001 From: fropych Date: Wed, 15 Jul 2026 10:31:18 +0300 Subject: [PATCH 1/2] Allow local LoRA discovery in offline mode --- src/diffusers/loaders/lora_base.py | 5 +- tests/lora/test_lora_loader_utils.py | 109 +++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 tests/lora/test_lora_loader_utils.py diff --git a/src/diffusers/loaders/lora_base.py b/src/diffusers/loaders/lora_base.py index 898eae0ca36b..dc01475ac8de 100644 --- a/src/diffusers/loaders/lora_base.py +++ b/src/diffusers/loaders/lora_base.py @@ -277,15 +277,14 @@ def _fetch_state_dict( def _best_guess_weight_name( pretrained_model_name_or_path_or_dict, file_extension=".safetensors", local_files_only=False ): - if local_files_only or HF_HUB_OFFLINE: - raise ValueError("When using the offline mode, you must specify a `weight_name`.") - targeted_files = [] if os.path.isfile(pretrained_model_name_or_path_or_dict): return elif os.path.isdir(pretrained_model_name_or_path_or_dict): targeted_files = [f for f in os.listdir(pretrained_model_name_or_path_or_dict) if f.endswith(file_extension)] + elif local_files_only or HF_HUB_OFFLINE: + raise ValueError("When using the offline mode, you must specify a `weight_name`.") else: files_in_repo = model_info(pretrained_model_name_or_path_or_dict).siblings targeted_files = [f.rfilename for f in files_in_repo if f.rfilename.endswith(file_extension)] diff --git a/tests/lora/test_lora_loader_utils.py b/tests/lora/test_lora_loader_utils.py new file mode 100644 index 000000000000..00ec526b388f --- /dev/null +++ b/tests/lora/test_lora_loader_utils.py @@ -0,0 +1,109 @@ +# coding=utf-8 +# Copyright 2026 HuggingFace Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +import torch +from safetensors.torch import save_file + +from diffusers.loaders import StableDiffusionLoraLoaderMixin +from diffusers.loaders.lora_base import _best_guess_weight_name + + +LORA_KEY = "unet.test.lora_A.weight" + + +def _write_lora_weights(path): + save_file({LORA_KEY: torch.ones(1)}, path) + + +class LoraWeightNameDiscoveryTests(unittest.TestCase): + def test_local_directory_in_offline_mode(self): + with tempfile.TemporaryDirectory() as tmpdirname: + _write_lora_weights(Path(tmpdirname) / "adapter.safetensors") + + with ( + patch("diffusers.loaders.lora_base.HF_HUB_OFFLINE", True), + patch("diffusers.loaders.lora_base.model_info") as model_info_mock, + ): + state_dict, _ = StableDiffusionLoraLoaderMixin.lora_state_dict(tmpdirname) + + self.assertTrue(torch.equal(state_dict[LORA_KEY], torch.ones(1))) + model_info_mock.assert_not_called() + + def test_local_directory_with_local_files_only(self): + with tempfile.TemporaryDirectory() as tmpdirname: + _write_lora_weights(Path(tmpdirname) / "adapter.safetensors") + + with patch("diffusers.loaders.lora_base.model_info") as model_info_mock: + state_dict, _ = StableDiffusionLoraLoaderMixin.lora_state_dict(tmpdirname, local_files_only=True) + + self.assertTrue(torch.equal(state_dict[LORA_KEY], torch.ones(1))) + model_info_mock.assert_not_called() + + def test_local_file_in_offline_mode(self): + with tempfile.TemporaryDirectory() as tmpdirname: + weight_path = Path(tmpdirname) / "adapter.safetensors" + _write_lora_weights(weight_path) + + with ( + patch("diffusers.loaders.lora_base.HF_HUB_OFFLINE", True), + patch("diffusers.loaders.lora_base.model_info") as model_info_mock, + ): + state_dict, _ = StableDiffusionLoraLoaderMixin.lora_state_dict(weight_path) + + self.assertTrue(torch.equal(state_dict[LORA_KEY], torch.ones(1))) + model_info_mock.assert_not_called() + + def test_remote_repository_in_offline_mode_requires_weight_name(self): + with ( + patch("diffusers.loaders.lora_base.HF_HUB_OFFLINE", True), + patch("diffusers.loaders.lora_base.model_info") as model_info_mock, + ): + with self.assertRaisesRegex(ValueError, "offline mode.*weight_name"): + StableDiffusionLoraLoaderMixin.lora_state_dict("organization/repository") + + model_info_mock.assert_not_called() + + def test_local_directory_without_matching_files_returns_none(self): + with tempfile.TemporaryDirectory() as tmpdirname: + (Path(tmpdirname) / "notes.txt").touch() + + with patch("diffusers.loaders.lora_base.HF_HUB_OFFLINE", True): + weight_name = _best_guess_weight_name(tmpdirname) + + self.assertIsNone(weight_name) + + def test_local_directory_with_multiple_files_warns_and_uses_first(self): + with tempfile.TemporaryDirectory() as tmpdirname: + first_path = Path(tmpdirname) / "first.safetensors" + second_path = Path(tmpdirname) / "second.safetensors" + first_path.touch() + second_path.touch() + + with ( + patch("diffusers.loaders.lora_base.HF_HUB_OFFLINE", True), + patch( + "diffusers.loaders.lora_base.os.listdir", + return_value=[first_path.name, second_path.name], + ), + self.assertLogs("diffusers.loaders.lora_base", level="WARNING") as logs, + ): + weight_name = _best_guess_weight_name(tmpdirname) + + self.assertEqual(weight_name, first_path.name) + self.assertIn("contains more than one weights file", logs.output[0]) From 5d297a0d8917b567bcb488daf4dc2657773f9523 Mon Sep 17 00:00:00 2001 From: fropych Date: Thu, 16 Jul 2026 12:05:17 +0300 Subject: [PATCH 2/2] Use pytest fixtures for LoRA loader tests --- tests/lora/test_lora_loader_utils.py | 157 +++++++++++++-------------- 1 file changed, 75 insertions(+), 82 deletions(-) diff --git a/tests/lora/test_lora_loader_utils.py b/tests/lora/test_lora_loader_utils.py index 00ec526b388f..b35ffea80768 100644 --- a/tests/lora/test_lora_loader_utils.py +++ b/tests/lora/test_lora_loader_utils.py @@ -12,16 +12,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import tempfile -import unittest -from pathlib import Path -from unittest.mock import patch +import logging +from unittest.mock import Mock +import pytest import torch from safetensors.torch import save_file -from diffusers.loaders import StableDiffusionLoraLoaderMixin -from diffusers.loaders.lora_base import _best_guess_weight_name +from diffusers.loaders import StableDiffusionLoraLoaderMixin, lora_base LORA_KEY = "unet.test.lora_A.weight" @@ -31,79 +29,74 @@ def _write_lora_weights(path): save_file({LORA_KEY: torch.ones(1)}, path) -class LoraWeightNameDiscoveryTests(unittest.TestCase): - def test_local_directory_in_offline_mode(self): - with tempfile.TemporaryDirectory() as tmpdirname: - _write_lora_weights(Path(tmpdirname) / "adapter.safetensors") - - with ( - patch("diffusers.loaders.lora_base.HF_HUB_OFFLINE", True), - patch("diffusers.loaders.lora_base.model_info") as model_info_mock, - ): - state_dict, _ = StableDiffusionLoraLoaderMixin.lora_state_dict(tmpdirname) - - self.assertTrue(torch.equal(state_dict[LORA_KEY], torch.ones(1))) - model_info_mock.assert_not_called() - - def test_local_directory_with_local_files_only(self): - with tempfile.TemporaryDirectory() as tmpdirname: - _write_lora_weights(Path(tmpdirname) / "adapter.safetensors") - - with patch("diffusers.loaders.lora_base.model_info") as model_info_mock: - state_dict, _ = StableDiffusionLoraLoaderMixin.lora_state_dict(tmpdirname, local_files_only=True) - - self.assertTrue(torch.equal(state_dict[LORA_KEY], torch.ones(1))) - model_info_mock.assert_not_called() - - def test_local_file_in_offline_mode(self): - with tempfile.TemporaryDirectory() as tmpdirname: - weight_path = Path(tmpdirname) / "adapter.safetensors" - _write_lora_weights(weight_path) - - with ( - patch("diffusers.loaders.lora_base.HF_HUB_OFFLINE", True), - patch("diffusers.loaders.lora_base.model_info") as model_info_mock, - ): - state_dict, _ = StableDiffusionLoraLoaderMixin.lora_state_dict(weight_path) - - self.assertTrue(torch.equal(state_dict[LORA_KEY], torch.ones(1))) - model_info_mock.assert_not_called() - - def test_remote_repository_in_offline_mode_requires_weight_name(self): - with ( - patch("diffusers.loaders.lora_base.HF_HUB_OFFLINE", True), - patch("diffusers.loaders.lora_base.model_info") as model_info_mock, - ): - with self.assertRaisesRegex(ValueError, "offline mode.*weight_name"): - StableDiffusionLoraLoaderMixin.lora_state_dict("organization/repository") - - model_info_mock.assert_not_called() - - def test_local_directory_without_matching_files_returns_none(self): - with tempfile.TemporaryDirectory() as tmpdirname: - (Path(tmpdirname) / "notes.txt").touch() - - with patch("diffusers.loaders.lora_base.HF_HUB_OFFLINE", True): - weight_name = _best_guess_weight_name(tmpdirname) - - self.assertIsNone(weight_name) - - def test_local_directory_with_multiple_files_warns_and_uses_first(self): - with tempfile.TemporaryDirectory() as tmpdirname: - first_path = Path(tmpdirname) / "first.safetensors" - second_path = Path(tmpdirname) / "second.safetensors" - first_path.touch() - second_path.touch() - - with ( - patch("diffusers.loaders.lora_base.HF_HUB_OFFLINE", True), - patch( - "diffusers.loaders.lora_base.os.listdir", - return_value=[first_path.name, second_path.name], - ), - self.assertLogs("diffusers.loaders.lora_base", level="WARNING") as logs, - ): - weight_name = _best_guess_weight_name(tmpdirname) - - self.assertEqual(weight_name, first_path.name) - self.assertIn("contains more than one weights file", logs.output[0]) +@pytest.fixture +def lora_weight_path(tmp_path): + weight_path = tmp_path / "adapter.safetensors" + _write_lora_weights(weight_path) + return weight_path + + +@pytest.fixture +def model_info_mock(monkeypatch): + model_info_mock = Mock() + monkeypatch.setattr(lora_base, "model_info", model_info_mock) + return model_info_mock + + +def test_local_directory_in_offline_mode(lora_weight_path, monkeypatch, model_info_mock): + monkeypatch.setattr(lora_base, "HF_HUB_OFFLINE", True) + + state_dict, _ = StableDiffusionLoraLoaderMixin.lora_state_dict(lora_weight_path.parent) + + assert torch.equal(state_dict[LORA_KEY], torch.ones(1)) + model_info_mock.assert_not_called() + + +def test_local_directory_with_local_files_only(lora_weight_path, model_info_mock): + state_dict, _ = StableDiffusionLoraLoaderMixin.lora_state_dict(lora_weight_path.parent, local_files_only=True) + + assert torch.equal(state_dict[LORA_KEY], torch.ones(1)) + model_info_mock.assert_not_called() + + +def test_local_file_in_offline_mode(lora_weight_path, monkeypatch, model_info_mock): + monkeypatch.setattr(lora_base, "HF_HUB_OFFLINE", True) + + state_dict, _ = StableDiffusionLoraLoaderMixin.lora_state_dict(lora_weight_path) + + assert torch.equal(state_dict[LORA_KEY], torch.ones(1)) + model_info_mock.assert_not_called() + + +def test_remote_repository_in_offline_mode_requires_weight_name(monkeypatch, model_info_mock): + monkeypatch.setattr(lora_base, "HF_HUB_OFFLINE", True) + + with pytest.raises(ValueError, match="offline mode.*weight_name"): + StableDiffusionLoraLoaderMixin.lora_state_dict("organization/repository") + + model_info_mock.assert_not_called() + + +def test_local_directory_without_matching_files_returns_none(tmp_path, monkeypatch): + (tmp_path / "notes.txt").touch() + monkeypatch.setattr(lora_base, "HF_HUB_OFFLINE", True) + + weight_name = lora_base._best_guess_weight_name(tmp_path) + + assert weight_name is None + + +def test_local_directory_with_multiple_files_warns_and_uses_first(tmp_path, monkeypatch, caplog): + first_path = tmp_path / "first.safetensors" + second_path = tmp_path / "second.safetensors" + first_path.touch() + second_path.touch() + monkeypatch.setattr(lora_base, "HF_HUB_OFFLINE", True) + monkeypatch.setattr(lora_base.os, "listdir", lambda _: [first_path.name, second_path.name]) + monkeypatch.setattr(lora_base.logger, "propagate", True) + + with caplog.at_level(logging.WARNING, logger="diffusers.loaders.lora_base"): + weight_name = lora_base._best_guess_weight_name(tmp_path) + + assert weight_name == first_path.name + assert "contains more than one weights file" in caplog.text