diff --git a/studio/backend/core/inference/diffusion.py b/studio/backend/core/inference/diffusion.py index e17be82c28..def744f878 100644 --- a/studio/backend/core/inference/diffusion.py +++ b/studio/backend/core/inference/diffusion.py @@ -1188,9 +1188,7 @@ class DiffusionBackend: "native engine for GGUF models." ) - resolved = diffusion_lora.resolve_specs( - specs, hf_token = state.hf_token, cancel_event = cancel - ) + resolved = diffusion_lora.resolve_specs(specs, hf_token = state.hf_token, cancel_event = cancel) # Unique adapter names (diffusers requires distinct names; sanitized stems can collide). uniq: list[tuple[str, str, float]] = [] seen: set[str] = set() diff --git a/studio/backend/core/inference/diffusion_lora.py b/studio/backend/core/inference/diffusion_lora.py index 22bfadf7c6..bb4fa1f0a9 100644 --- a/studio/backend/core/inference/diffusion_lora.py +++ b/studio/backend/core/inference/diffusion_lora.py @@ -243,7 +243,9 @@ def materialize_native_dir(resolved: list[ResolvedLora], dest: Path) -> list[Res n += 1 alias = f"{r.alias}_{n}" used.add(alias) - ext = os.path.splitext(r.path)[1].lower() or (".gguf" if r.fmt == "gguf" else ".safetensors") + ext = os.path.splitext(r.path)[1].lower() or ( + ".gguf" if r.fmt == "gguf" else ".safetensors" + ) link = dest / f"{alias}{ext}" try: if link.exists() or link.is_symlink(): @@ -251,7 +253,6 @@ def materialize_native_dir(resolved: list[ResolvedLora], dest: Path) -> list[Res os.symlink(os.path.realpath(r.path), link) except OSError: import shutil - shutil.copy2(r.path, link) out.append(ResolvedLora(r.id, alias, str(link), r.fmt, r.weight)) return out @@ -286,7 +287,16 @@ def _fmt_weight(w: float) -> str: # Families the native sd-cli LoRA name-conversion supports (SD1.5/SD2/SDXL/SD3/FLUX/ # z-image). Qwen-Image has no LoRA branch in stable-diffusion.cpp -> excluded until # validated. Matched by substring against the resolved family name. -_NATIVE_LORA_FAMILY_TOKENS = ("flux.1", "flux.2", "z-image", "sd1", "sd2", "sdxl", "sd3", "stable-diffusion") +_NATIVE_LORA_FAMILY_TOKENS = ( + "flux.1", + "flux.2", + "z-image", + "sd1", + "sd2", + "sdxl", + "sd3", + "stable-diffusion", +) # Diffusers quant schemes that cannot take LoRA cleanly (torchao tensor-subclass weights). _DIFFUSERS_LORA_BLOCKED_QUANT = ("int8", "fp8", "nvfp4", "mxfp8") diff --git a/studio/backend/tests/test_diffusion_lora.py b/studio/backend/tests/test_diffusion_lora.py index f0016d8cb5..4bf8b85f0e 100644 --- a/studio/backend/tests/test_diffusion_lora.py +++ b/studio/backend/tests/test_diffusion_lora.py @@ -42,15 +42,31 @@ def test_inject_prompt_tags_empty_returns_prompt(): def test_supports_lora_matrix(): # native: flux/z-image yes, qwen no - assert dl.supports_lora(engine = "sd_cpp", family = "flux.1", model_kind = "gguf", transformer_quant = None) - assert dl.supports_lora(engine = "sd_cpp", family = "z-image", model_kind = "gguf", transformer_quant = None) - assert not dl.supports_lora(engine = "sd_cpp", family = "qwen-image", model_kind = "gguf", transformer_quant = None) + assert dl.supports_lora( + engine = "sd_cpp", family = "flux.1", model_kind = "gguf", transformer_quant = None + ) + assert dl.supports_lora( + engine = "sd_cpp", family = "z-image", model_kind = "gguf", transformer_quant = None + ) + assert not dl.supports_lora( + engine = "sd_cpp", family = "qwen-image", model_kind = "gguf", transformer_quant = None + ) # diffusers: bf16 yes, fp8/int8 dense no, gguf-diffusers no - assert dl.supports_lora(engine = "diffusers", family = "flux.1", model_kind = "pipeline", transformer_quant = None) - assert dl.supports_lora(engine = "diffusers", family = "flux.1", model_kind = "single_file", transformer_quant = None) - assert not dl.supports_lora(engine = "diffusers", family = "flux.1", model_kind = "single_file", transformer_quant = "fp8") - assert not dl.supports_lora(engine = "diffusers", family = "flux.1", model_kind = "single_file", transformer_quant = "int8") - assert not dl.supports_lora(engine = "diffusers", family = "flux.1", model_kind = "gguf", transformer_quant = None) + assert dl.supports_lora( + engine = "diffusers", family = "flux.1", model_kind = "pipeline", transformer_quant = None + ) + assert dl.supports_lora( + engine = "diffusers", family = "flux.1", model_kind = "single_file", transformer_quant = None + ) + assert not dl.supports_lora( + engine = "diffusers", family = "flux.1", model_kind = "single_file", transformer_quant = "fp8" + ) + assert not dl.supports_lora( + engine = "diffusers", family = "flux.1", model_kind = "single_file", transformer_quant = "int8" + ) + assert not dl.supports_lora( + engine = "diffusers", family = "flux.1", model_kind = "gguf", transformer_quant = None + ) def test_materialize_native_dir_symlinks_and_breaks_collisions(tmp_path): @@ -136,10 +152,18 @@ class _FakePipe: self.active = None self.unloaded = 0 - def load_lora_weights(self, path, adapter_name = None): + def load_lora_weights( + self, + path, + adapter_name = None, + ): self.loaded.append((path, adapter_name)) - def set_adapters(self, names, adapter_weights = None): + def set_adapters( + self, + names, + adapter_weights = None, + ): self.active = (list(names), list(adapter_weights) if adapter_weights else None) def unload_lora_weights(self): @@ -148,7 +172,12 @@ class _FakePipe: self.active = None -def _fake_state(pipe, *, kind = "pipeline", quant = None): +def _fake_state( + pipe, + *, + kind = "pipeline", + quant = None, +): fam = types.SimpleNamespace(name = "flux.1") return types.SimpleNamespace( pipe = pipe, family = fam, kind = kind, transformer_quant = quant, hf_token = None @@ -157,7 +186,6 @@ def _fake_state(pipe, *, kind = "pipeline", quant = None): def _backend(): from core.inference.diffusion import DiffusionBackend - return DiffusionBackend() @@ -173,7 +201,9 @@ def test_diffusers_apply_loads_and_sets_adapters(monkeypatch): ], ) pipe = _FakePipe() - _backend()._apply_loras(_fake_state(pipe), [("styleA", 0.8), ("styleB", 1.0)], threading.Event()) + _backend()._apply_loras( + _fake_state(pipe), [("styleA", 0.8), ("styleB", 1.0)], threading.Event() + ) assert [n for _p, n in pipe.loaded] == ["styleA", "styleB"] assert pipe.active == (["styleA", "styleB"], [0.8, 1.0]) assert getattr(pipe, "_unsloth_loras") # marker recorded @@ -220,7 +250,6 @@ def test_diffusers_apply_clears_when_empty(monkeypatch): def test_diffusers_apply_rejects_unsupported_quant(): import threading - pipe = _FakePipe() with pytest.raises(ValueError, match = "not supported"): _backend()._apply_loras(