diff --git a/studio/backend/core/inference/diffusion.py b/studio/backend/core/inference/diffusion.py
index 3a2cc8d503..9e0ee871fd 100644
--- a/studio/backend/core/inference/diffusion.py
+++ b/studio/backend/core/inference/diffusion.py
@@ -115,15 +115,25 @@ _FAMILIES: tuple[DiffusionFamily, ...] = (
base_repo = "stabilityai/stable-diffusion-3-medium-diffusers",
aliases = ("sd3-medium", "stable-diffusion-3-medium", "sd3.5"),
),
+ # SDXL: full diffusers path only (no GGUF). SDXL uses a UNet (not a
+ # transformer) and wiring UNet2DConditionModel.from_single_file +
+ # GGUF is a separate code path the rest of this module does not
+ # exercise. The family is intentionally NOT in _FAMILIES so the
+ # frontend status panel does not advertise GGUF support we do not
+ # implement; callers wanting SDXL full repos can still do so by
+ # passing the diffusers repo with no gguf_filename and
+ # family_override = "stable-diffusion-xl" via the route, which uses
+ # the lookup in _FULL_REPO_FAMILIES.
+)
+
+
+# Families available via family_override on the routes layer when the
+# user is loading a full diffusers checkpoint (no GGUF). Kept separate
+# from _FAMILIES so the GGUF-only status panel does not over-advertise.
+_FULL_REPO_FAMILIES: tuple[DiffusionFamily, ...] = (
DiffusionFamily(
name = "stable-diffusion-xl",
pipeline_class = "StableDiffusionXLPipeline",
- # SDXL uses a UNet, not a transformer. Loading SDXL GGUFs would
- # require UNet2DConditionModel.from_single_file + GGUF, which is
- # not the same code path as the FLUX / Qwen-Image transformers
- # this PR ships. Until that path is wired and smoke-tested,
- # treat SDXL as full-repo-only and surface a clear error when a
- # user tries to pass gguf_filename for it.
transformer_class = "",
base_repo = "stabilityai/stable-diffusion-xl-base-1.0",
aliases = ("sdxl",),
@@ -137,13 +147,14 @@ def detect_family(
"""Return the diffusion family matching ``repo_id``.
Matching is substring-based and case-insensitive. ``override_family``
- bypasses substring matching and looks up by ``DiffusionFamily.name``.
- Returns ``None`` when no family applies so callers can surface a clear
- "unsupported model" error rather than guessing wrong.
+ bypasses substring matching and looks up by ``DiffusionFamily.name``
+ or (when explicitly asked) by ``_FULL_REPO_FAMILIES.name``.
+ Returns ``None`` when no family applies so callers can surface a
+ clear "unsupported model" error rather than guessing wrong.
"""
if override_family:
wanted = override_family.strip().lower()
- for fam in _FAMILIES:
+ for fam in _FAMILIES + _FULL_REPO_FAMILIES:
if fam.name == wanted:
return fam
return None
@@ -395,17 +406,21 @@ class DiffusionBackend:
self._loading = False
def unload_model(self) -> dict[str, Any]:
- with self._lock:
- old = self._pipe
- self._pipe = None
- self._family = None
- self._repo_id = None
- self._gguf_path = None
- self._base_repo = None
- self._device = None
- self._dtype = None
- self._loaded_at = None
- _release(old)
+ # Take the load lock too so unload cannot race with an in-flight
+ # load_model and have the load thread overwrite the cleared state
+ # after we already returned {"is_loaded": false}.
+ with self._load_lock:
+ with self._lock:
+ old = self._pipe
+ self._pipe = None
+ self._family = None
+ self._repo_id = None
+ self._gguf_path = None
+ self._base_repo = None
+ self._device = None
+ self._dtype = None
+ self._loaded_at = None
+ _release(old)
return {"is_loaded": False}
# ── generation ────────────────────────────────────────────────
diff --git a/studio/frontend/src/features/images/images-page.tsx b/studio/frontend/src/features/images/images-page.tsx
index a422b59bbc..abca68c0f2 100644
--- a/studio/frontend/src/features/images/images-page.tsx
+++ b/studio/frontend/src/features/images/images-page.tsx
@@ -458,7 +458,10 @@ export function ImagesPage() {