{"record":{"id":"81a12222ea436000","repo":"unslothai/unsloth","slug":"could-not-switch-the-diffusion-engine-to-name-u","errorCode":null,"errorMessage":"Could not switch the diffusion engine to {name}: unloading the current {old_name} model failed ({exc}). The current model is still loaded; unload it and try again.","messagePattern":"Could not switch the diffusion engine to (.+?): unloading the current (.+?) model failed \\((.+?)\\)\\. The current model is still loaded; unload it and try again\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":409,"severity":"error","filePath":"studio/backend/core/inference/diffusion_engine_router.py","lineNumber":122,"sourceCode":"        old_name = None\n        with _lock:\n            if name != _active_engine_name:\n                engine_to_unload = get_active_diffusion_engine()\n                old_name = _active_engine_name\n            else:\n                # No engine change: publish the (possibly refreshed) fallback reason now.\n                _fallback_reason = reason if name == ENGINE_DIFFUSERS else None\n        if engine_to_unload is not None:\n            # Publish the new engine only AFTER the old one unloads: the evictor unloads get_active_diffusion_engine(), so flipping\n            # the name first would let a concurrent acquire_for evict the new (empty) engine while the old model frees VRAM.\n            try:\n                engine_to_unload.unload()\n            except Exception as exc:\n                # Do NOT publish the new engine after a failed teardown. The old model (or the resident sd-server) still holds its memory, and flipping the\n                # name would hide it from get_active_diffusion_engine(), which the evictor, /images/unload and the next load all resolve through, so the leak\n                # would be permanent. Leaving the old engine active keeps it reclaimable and lets the caller retry.\n                logger.error(\"failed to unload previous engine %s: %s\", old_name, exc)\n                raise RuntimeError(\n                    f\"Could not switch the diffusion engine to {name}: unloading the current \"\n                    f\"{old_name} model failed ({exc}). The current model is still loaded; \"\n                    \"unload it and try again.\"\n                ) from exc\n            with _lock:\n                _active_engine_name = name\n                _fallback_reason = reason if name == ENGINE_DIFFUSERS else None\n        if name == ENGINE_SD_CPP:\n            logger.info(\"diffusion engine: sd_cpp\")\n        else:\n            logger.info(\"diffusion engine: diffusers (%s)\", reason or \"selected\")\n        return get_active_diffusion_engine()\n\n\ndef begin_load_on(expected_engine: Any, start: Callable[[], Any]) -> Any:\n    \"\"\"Run ``start`` under the transition lock, refusing if the engine changed since selection.\n\n    A load route selects its engine, then yields (device probe, arbiter acquire) before it","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/inference/diffusion_engine_router.py#L104-L140","documentation":"Switching the active diffusion engine requires unloading the current one; that unload raised. The router deliberately does NOT publish the new engine after a failed teardown, because the old model still holds VRAM and flipping the name would hide it from get_active_diffusion_engine() — making the leak permanent. The old engine stays active and reclaimable so the caller can retry.","triggerScenarios":"A second /images/load (or any engine switch) targeting the other engine while the current engine's unload() raises — e.g. sd-server resident process refusing to stop, CUDA errors during model free, or a diffusers pipeline teardown exception.","commonSituations":"Two concurrent loads racing across engines; a wedged resident sd-server process; transient CUDA/driver errors while freeing VRAM; repeated engine flip-flopping under load.","solutions":["Retry the switch after manually unloading: call the unload endpoint (/images/unload) or otherwise free the current model, then reissue the load","If unload keeps failing, restart the backend process to clear the wedged resident model","Check logs for the underlying unload exception ('failed to unload previous engine ...') and fix that root cause (kill a stuck sd-server, resolve CUDA errors)"],"exampleFix":"# before\nengine = select_and_activate_engine(...)  # RuntimeError: unload failed\nengine = select_and_activate_engine(...)  # blind retry, same failure\n\n# after\ntry:\n    engine = select_and_activate_engine(...)\nexcept RuntimeError:\n    unload_current_model()          # /images/unload or get_active_diffusion_engine().unload()\n    engine = select_and_activate_engine(...)  # now the old engine is gone, switch succeeds","handlingStrategy":"retry","validationCode":"def engine_switch_safe() -> bool:\n    # best-effort preflight: current engine can unload without CUDA/resident issues\n    eng = get_active_diffusion_engine()\n    try:\n        return eng is None or eng.can_unload() if hasattr(eng, \"can_unload\") else True\n    except Exception:\n        return False","typeGuard":null,"tryCatchPattern":"try:\n    engine = select_and_activate_engine(fam, ...)\nexcept RuntimeError as e:\n    if \"unload it and try again\" in str(e):\n        get_active_diffusion_engine().unload()  # or POST /images/unload\n        engine = select_and_activate_engine(fam, ...)  # retry once\n    else:\n        raise","preventionTips":["Avoid concurrent engine-switching loads; serialize /images/load per host","Unload the current model explicitly before switching engines in operational scripts","Watch for 'failed to unload previous engine' in logs — that chained exception is the real fault to fix"],"tags":["engine-switch","unload-failure","vram","concurrency","diffusion"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}