diff --git a/studio/backend/core/inference/diffusion.py b/studio/backend/core/inference/diffusion.py index 8f7291cffa..7bd7180b53 100644 --- a/studio/backend/core/inference/diffusion.py +++ b/studio/backend/core/inference/diffusion.py @@ -488,6 +488,16 @@ class DiffusionBackend: ) elif path_shaped: raise FileNotFoundError(f"Local model path does not exist: {repo_id}") + elif repo_id.upper().endswith("-GGUF"): + # A remote "*-GGUF" id is a single-file GGUF repo, not a full diffusers + # pipeline: loading it as a pipeline passes the trusted-repo check, evicts + # chat, then fails in the background when from_pretrained finds no + # model_index.json. Reject the certain case here (no network round-trip) + # so the bad pick fails before the GPU handoff, as the route expects. + raise ValueError( + f"'{repo_id}' is a single-file GGUF repo; load it with model_kind 'gguf' " + f"and a .gguf filename, not as a full pipeline." + ) return fam # ── Background load + progress ───────────────────────────────────────── @@ -1348,7 +1358,14 @@ class DiffusionBackend: # reuse the resident modules AT THEIR LOADED dtype, which is the whole point of # from_pipe (component reuse, no reload, no extra VRAM). pipe = getattr(diffusers, class_name).from_pipe(state.pipe, torch_dtype = None) - self._aux_pipes[class_name] = pipe + # Only publish to the shared aux cache if THIS load is still current. from_pipe runs + # under _generate_lock but NOT _lock, so an unload()/superseding load can clear + # _aux_pipes and null _state while it builds; caching unconditionally would re-insert + # a wrapper over now-stale modules that a later same-workflow load would reuse (or + # keep the old VRAM pinned). This generation still uses the returned pipe. + with self._lock: + if self._state is state: + self._aux_pipes[class_name] = pipe return pipe @staticmethod diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 084d892a07..30f35cb9de 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -11199,8 +11199,13 @@ async def generate_diffusion_image( { "prompt": request.prompt, "negative_prompt": request.negative_prompt, - "width": request.width, - "height": request.height, + # Persist the ACTUAL output size, not the request sliders: Transform/ + # Inpaint/Edit derive it from the uploaded image, Extend grows the + # canvas, and Upscale resizes it, so request.width/height would record + # (and later restore) the wrong dimensions for those workflows. For + # plain txt2img the image size equals the sliders anyway. + "width": getattr(image, "width", None) or request.width, + "height": getattr(image, "height", None) or request.height, "steps": request.steps, "guidance": request.guidance, "seed": seed, diff --git a/studio/backend/tests/test_diffusion_backend.py b/studio/backend/tests/test_diffusion_backend.py index d1cbf513d0..ce1ced8352 100644 --- a/studio/backend/tests/test_diffusion_backend.py +++ b/studio/backend/tests/test_diffusion_backend.py @@ -1461,6 +1461,11 @@ def test_validate_load_request(tmp_path): backend.validate_load_request( "unsloth/Qwen-Image-2512-FP8", gguf_filename = "q.gguf", model_kind = "single_file" ) + # A remote "*-GGUF" repo loaded as a full pipeline (no single-file name) is a single-file + # GGUF repo, so from_pretrained would find no pipeline manifest and fail after chat is + # already evicted; reject it here before the GPU handoff. + with pytest.raises(ValueError, match = "GGUF"): + backend.validate_load_request("unsloth/Z-Image-Turbo-GGUF", model_kind = "pipeline") # A local path with a missing child fails here (before any GPU/network work). with pytest.raises(FileNotFoundError): backend.validate_load_request( diff --git a/studio/install_sd_cpp_prebuilt.py b/studio/install_sd_cpp_prebuilt.py index 193c5e4670..639e3a894d 100644 --- a/studio/install_sd_cpp_prebuilt.py +++ b/studio/install_sd_cpp_prebuilt.py @@ -262,6 +262,10 @@ def _maybe_fetch_windows_cudart(release: dict, chosen: str, target: Path) -> Non print(f"downloading CUDA runtime {cudart['name']} ...", flush = True) try: _download(cudart["browser_download_url"], dest) + # Verify integrity BEFORE extracting, like the main sd-cli archive: these DLLs are + # loaded into sd-cli.exe at runtime, so a corrupt/tampered runtime archive must be + # rejected rather than extracted next to the binary. + _verify_sha256(dest, cudart.get("digest")) with zipfile.ZipFile(dest) as zf: _safe_extractall(zf, target) finally: