Spaces:
Running on Zero
Running on Zero
| """The stateful engine: resolve the device once, load each model once, hold the optional recogniser. | |
| The UI and the API share one :class:`StudioEngine` for the process lifetime. Models are cached by weights | |
| path so clicking a checkpoint twice doesn't reload it; the cache is capped (default 2, enough for the | |
| single-line A/B compare) and evicts oldest-first with a CUDA cache flush, so switching through many | |
| checkpoints never grows VRAM without bound and never OOMs the co-tenant training run. | |
| """ | |
| from __future__ import annotations | |
| from typing import TYPE_CHECKING | |
| from diffu_studio.checkpoints import ArchConfig, LoadedModel, load_model, resolve_weights | |
| from diffu_studio.device import Device, resolve_device | |
| from diffu_studio.line import Recognizer, load_recognizer | |
| if TYPE_CHECKING: | |
| from diffu.config import Config | |
| class StudioEngine: | |
| """Owns the device + a small model cache + the read-back recogniser for the app's lifetime.""" | |
| def __init__( | |
| self, | |
| *, | |
| device: Device | None = None, | |
| ckpt_root: str = "checkpoints", | |
| arch: ArchConfig | None = None, | |
| recognizer_name: str | None = None, | |
| max_cached_models: int = 2, | |
| ) -> None: | |
| self.device = device or resolve_device() | |
| self.ckpt_root = ckpt_root | |
| self.arch = arch or ArchConfig() | |
| self.max_cached_models = max_cached_models | |
| self._models: dict[str, LoadedModel] = {} | |
| self._recognizer: Recognizer | None = ( | |
| load_recognizer(recognizer_name, self.device.torch_device) if recognizer_name else None | |
| ) | |
| def recognizer(self) -> Recognizer | None: | |
| return self._recognizer | |
| def model(self, path: str) -> LoadedModel: | |
| """Return the model at ``path`` (weights file or ``step_*`` dir), loading + caching it on first use. | |
| Raises :class:`~diffu_studio.checkpoints.CheckpointMismatch` if the checkpoint doesn't fit the code. | |
| """ | |
| key = resolve_weights(path) | |
| cached = self._models.get(key) | |
| if cached is not None: | |
| return cached | |
| loaded = load_model(path, self.arch, self.device) | |
| # ZeroGPU: opt-in AoT compile of the backbone (aokit). OFF by default (set DIFFU_STUDIO_AOT=1) until | |
| # the compiled graph's persistence across ZeroGPU's per-request forks is validated — the compile runs | |
| # in a @spaces.GPU fork whose install would not survive to the parent. Fallback-safe regardless. | |
| import os | |
| if os.environ.get("DIFFU_STUDIO_AOT") == "1": | |
| from diffu_studio.optimization import aot_compile_backbone | |
| aot_compile_backbone(loaded) | |
| self._models[key] = loaded | |
| self._evict_overflow() | |
| return loaded | |
| def config_for(self, path: str) -> Config: | |
| return self.model(path).cfg | |
| def _evict_overflow(self) -> None: | |
| while len(self._models) > self.max_cached_models: | |
| oldest = next(iter(self._models)) | |
| del self._models[oldest] | |
| _free_cuda() | |
| def _free_cuda() -> None: | |
| try: | |
| import torch | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| except Exception: # noqa: BLE001 — freeing cache is best-effort; never fail an eviction on it | |
| pass | |