diffu_test / diffu_studio /device.py
Gabriel's picture
Bundle: UI (Generate-up/collapsed/bigger style) + model preload + AoT off-by-default + PiD 4x upscale button
75c39e8 verified
Raw
History Blame Contribute Delete
4.61 kB
"""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)
@property
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)