Spaces:
Running on Zero
Running on Zero
| """Device backend — pick where inference runs and adapt the pipeline to it. | |
| Three targets, auto-detected (override with ``DIFFU_STUDIO_DEVICE=zerogpu|gpu|cpu`` or the CLI flag): | |
| - **zerogpu** — Hugging Face Spaces on-demand GPU. GPU work must be wrapped in ``@spaces.GPU``; that is | |
| what :func:`gpu_task` does (and it supports *generator* functions, so streaming keeps the GPU). | |
| - **gpu** — a local CUDA GPU (the training box: pin to GPU0, memory-capped, never OOM the training run). | |
| - **cpu** — fallback / Spaces-CPU. Correct but NOT interactive for whole pages (a ~2B-param diffusion | |
| backbone over N steps can't stream a page at "live" speed), so the UI shows progress, not animation. | |
| Nothing here imports torch at module load — a mismatched checkpoint or a CPU box must still be able to | |
| import the package and read the config. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from collections.abc import Callable | |
| from enum import StrEnum | |
| from typing import TYPE_CHECKING | |
| from pydantic import BaseModel, ConfigDict | |
| if TYPE_CHECKING: | |
| import torch | |
| class DeviceKind(StrEnum): | |
| zerogpu = "zerogpu" | |
| gpu = "gpu" | |
| cpu = "cpu" | |
| class Device(BaseModel): | |
| """Resolved inference target: where tensors live, in what dtype, and whether it can stream live.""" | |
| model_config = ConfigDict(frozen=True) | |
| kind: DeviceKind | |
| torch_device: str # "cuda" | "cpu" | |
| dtype: str # a torch dtype name: "float32" | "bfloat16" | |
| interactive: bool # fast enough to animate a page line-by-line (else show a progress bar) | |
| def label(self) -> str: | |
| return f"{self.kind.value} · {self.torch_device} · {self.dtype}" | |
| def resolve_device(override: str | None = None) -> Device: | |
| """Resolve the inference device from an explicit choice, then ``DIFFU_STUDIO_DEVICE``, then autodetect.""" | |
| choice = (override or os.environ.get("DIFFU_STUDIO_DEVICE") or "auto").lower() | |
| kind = _detect(choice) | |
| if kind is DeviceKind.cpu: | |
| return Device(kind=kind, torch_device="cpu", dtype="float32", interactive=False) | |
| # gpu and zerogpu both run on CUDA. Default to float32 — the proven, correctness-first path; bf16 is an | |
| # opt-in speed/VRAM lever (DIFFU_STUDIO_DTYPE=bfloat16), kept out of the default so page CER stays put. | |
| dtype = "bfloat16" if os.environ.get("DIFFU_STUDIO_DTYPE") == "bfloat16" else "float32" | |
| return Device(kind=kind, torch_device="cuda", dtype=dtype, interactive=True) | |
| def _detect(choice: str) -> DeviceKind: | |
| if choice in (DeviceKind.zerogpu, DeviceKind.gpu, DeviceKind.cpu): | |
| return DeviceKind(choice) | |
| if _on_zerogpu(): | |
| return DeviceKind.zerogpu | |
| return DeviceKind.gpu if _cuda_available() else DeviceKind.cpu | |
| def _on_zerogpu() -> bool: | |
| return bool(os.environ.get("SPACES_ZERO_GPU")) | |
| def _cuda_available() -> bool: | |
| try: | |
| import torch | |
| except ImportError: | |
| return False | |
| return torch.cuda.is_available() | |
| def torch_dtype(device: Device) -> torch.dtype: | |
| """The ``torch`` dtype object named by ``device.dtype`` (e.g. ``torch.float32``).""" | |
| import torch | |
| return getattr(torch, device.dtype) | |
| def configure_memory() -> None: | |
| """Cap CUDA fragmentation so the studio coexists with the training run. Must run BEFORE torch is imported | |
| (the allocator reads this at init), so the CLI calls it as its first line.""" | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| def gpu_task[**P, R]( | |
| func: Callable[P, R] | None = None, *, duration: int | None = None | |
| ) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]: | |
| """Wrap a GPU entry point so ZeroGPU allocates a GPU while it runs; a no-op off Spaces. | |
| ``spaces.GPU`` supports generator functions, so a streaming ``stream_line`` / ``stream_page`` keeps the | |
| GPU allocated across all its yields. Off ZeroGPU (local gpu / cpu) the function is returned unchanged. | |
| Usable bare (``gpu_task(fn)`` / ``@gpu_task``) or with a ZeroGPU time budget in seconds | |
| (``gpu_task(fn, duration=90)`` / ``@gpu_task(duration=90)``) for a long call like the PiD upscale. | |
| """ | |
| def wrap(f: Callable[P, R]) -> Callable[P, R]: | |
| if not _on_zerogpu(): | |
| return f | |
| import importlib | |
| try: # `spaces` only exists inside a HF Spaces runtime — import by name so it's not a hard dep | |
| spaces = importlib.import_module("spaces") | |
| except ImportError: | |
| return f | |
| return spaces.GPU(f, duration=duration) if duration is not None else spaces.GPU(f) | |
| return wrap if func is None else wrap(func) | |