Spaces:
Sleeping
Sleeping
try device in the order of gpu/mps/cpu
Browse files- README.md +1 -1
- app.py +62 -9
- generate.py +29 -21
README.md
CHANGED
|
@@ -44,7 +44,7 @@ python download_celeba_hq.py 42 --split train --output face-42.png
|
|
| 44 |
|
| 45 |
Push this repository to a Gradio Space and select ZeroGPU in the Space hardware settings. The Dockerfile and legacy server are not used by the Gradio deployment. Account eligibility and daily GPU quotas apply.
|
| 46 |
|
| 47 |
-
The app imports `spaces` before PyTorch
|
| 48 |
|
| 49 |
For persistent caching, attach a writable storage bucket and set the Space variable `MODEL_CACHE_DIR` to its mount path (for example `/data/models`). The old Docker path `/app/models` is also usable if explicitly configured with that variable.
|
| 50 |
|
|
|
|
| 44 |
|
| 45 |
Push this repository to a Gradio Space and select ZeroGPU in the Space hardware settings. The Dockerfile and legacy server are not used by the Gradio deployment. Account eligibility and daily GPU quotas apply.
|
| 46 |
|
| 47 |
+
The app imports `spaces` before PyTorch and places the GPU model on CUDA at startup when `SPACES_ZERO_GPU` is enabled. Reverse sampling tries GPU first and automatically restarts on CPU if GPU allocation, quota, or supported CUDA availability errors prevent the run. Locally, the app prefers CUDA, then Apple Silicon MPS, then CPU. MPS availability failures also fall back to CPU. Separate pipelines are cached for each device; the CPU pipeline loads on first use. CPU sampling is substantially slower. GPU sampling runs inside a decorated function called by the Gradio handler. `GPU_DURATION_SECONDS` defaults to 120; benchmark a full 1,000-step run and adjust this Space variable if necessary. A timeout is reported by the Gradio queue; selecting fewer checkpoints does not reduce the sampling steps.
|
| 48 |
|
| 49 |
For persistent caching, attach a writable storage bucket and set the Space variable `MODEL_CACHE_DIR` to its mount path (for example `/data/models`). The old Docker path `/app/models` is also usable if explicitly configured with that variable.
|
| 50 |
|
app.py
CHANGED
|
@@ -16,7 +16,32 @@ from PIL import Image
|
|
| 16 |
from generate import CHECKPOINTS, ROOT, generate_images, load_model
|
| 17 |
|
| 18 |
ZERO_GPU = os.environ.get("SPACES_ZERO_GPU", "").lower() in {"1", "true", "yes"}
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
def parse_settings(seed, checkpoint_text):
|
|
@@ -81,23 +106,51 @@ def diffuse(image, seed, checkpoint_text):
|
|
| 81 |
yield checkpoint_slots(checkpoints, frames), final_noise if step == 1000 else None, progress_html(step, started)
|
| 82 |
|
| 83 |
|
| 84 |
-
|
| 85 |
-
def denoise(seed, checkpoint_text, input_mode, forward_noise):
|
| 86 |
-
seed, checkpoints = parse_settings(seed, checkpoint_text)
|
| 87 |
-
initial_image = forward_noise if input_mode == "Forward noise" else None
|
| 88 |
-
if input_mode == "Forward noise" and initial_image is None:
|
| 89 |
-
raise gr.Error("Run Diffuse first to create forward noise.")
|
| 90 |
started = time.monotonic()
|
| 91 |
frames = {1000: initial_image if initial_image is not None else gaussian_noise(seed)}
|
| 92 |
yield checkpoint_slots(checkpoints, frames), progress_html(0, started)
|
| 93 |
-
|
|
|
|
| 94 |
if frame is not None:
|
| 95 |
frames[step] = frame
|
| 96 |
-
# Stream every pipeline status; only resend gallery images at checkpoints.
|
| 97 |
yield (checkpoint_slots(checkpoints, frames) if frame is not None else gr.skip(),
|
| 98 |
progress_html(1000 - step, started))
|
| 99 |
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
# Reuse the original page's palette, typography, header, and panel styling.
|
| 102 |
PAGE_CSS = (ROOT / "styles.css").read_text() + """
|
| 103 |
.gradio-container { max-width:1440px !important; padding:0 6% !important;
|
|
|
|
| 16 |
from generate import CHECKPOINTS, ROOT, generate_images, load_model
|
| 17 |
|
| 18 |
ZERO_GPU = os.environ.get("SPACES_ZERO_GPU", "").lower() in {"1", "true", "yes"}
|
| 19 |
+
def gpu_unavailable(error):
|
| 20 |
+
"""Recognize allocation/quota/CUDA failures without masking model errors."""
|
| 21 |
+
message = str(error).lower()
|
| 22 |
+
return any(text in message for text in (
|
| 23 |
+
"quota exceeded", "exceeded your gpu quota", "exceeded your zerogpu quota",
|
| 24 |
+
"no gpu was available", "no gpu available", "cuda out of memory",
|
| 25 |
+
"cuda is not available", "no nvidia driver", "not compiled with cuda",
|
| 26 |
+
"cuda driver", "cuda-capable device", "gpu task aborted",
|
| 27 |
+
))
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ZeroGPU requires CUDA model placement before its worker is allocated.
|
| 31 |
+
GPU_READY = False
|
| 32 |
+
MPS_READY = False
|
| 33 |
+
if ZERO_GPU:
|
| 34 |
+
try:
|
| 35 |
+
load_model(device="cuda")
|
| 36 |
+
GPU_READY = True
|
| 37 |
+
except Exception as error:
|
| 38 |
+
if not gpu_unavailable(error):
|
| 39 |
+
raise
|
| 40 |
+
print(f"GPU unavailable at startup; using CPU: {error}")
|
| 41 |
+
else:
|
| 42 |
+
import torch
|
| 43 |
+
GPU_READY = torch.cuda.is_available()
|
| 44 |
+
MPS_READY = hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
|
| 45 |
|
| 46 |
|
| 47 |
def parse_settings(seed, checkpoint_text):
|
|
|
|
| 106 |
yield checkpoint_slots(checkpoints, frames), final_noise if step == 1000 else None, progress_html(step, started)
|
| 107 |
|
| 108 |
|
| 109 |
+
def stream_denoising(seed, checkpoints, initial_image, device):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
started = time.monotonic()
|
| 111 |
frames = {1000: initial_image if initial_image is not None else gaussian_noise(seed)}
|
| 112 |
yield checkpoint_slots(checkpoints, frames), progress_html(0, started)
|
| 113 |
+
print('using', device)
|
| 114 |
+
for step, frame in generate_images(seed, checkpoints, initial_image, device=device):
|
| 115 |
if frame is not None:
|
| 116 |
frames[step] = frame
|
|
|
|
| 117 |
yield (checkpoint_slots(checkpoints, frames) if frame is not None else gr.skip(),
|
| 118 |
progress_html(1000 - step, started))
|
| 119 |
|
| 120 |
|
| 121 |
+
@spaces.GPU(duration=int(os.environ.get("GPU_DURATION_SECONDS", "120")))
|
| 122 |
+
def denoise_gpu(seed, checkpoints, initial_image):
|
| 123 |
+
yield from stream_denoising(seed, checkpoints, initial_image, "cuda")
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def denoise(seed, checkpoint_text, input_mode, forward_noise):
|
| 127 |
+
seed, checkpoints = parse_settings(seed, checkpoint_text)
|
| 128 |
+
initial_image = forward_noise if input_mode == "Forward noise" else None
|
| 129 |
+
if input_mode == "Forward noise" and initial_image is None:
|
| 130 |
+
raise gr.Error("Run Diffuse first to create forward noise.")
|
| 131 |
+
if GPU_READY:
|
| 132 |
+
try:
|
| 133 |
+
yield from denoise_gpu(seed, checkpoints, initial_image)
|
| 134 |
+
return
|
| 135 |
+
except Exception as error:
|
| 136 |
+
if not gpu_unavailable(error):
|
| 137 |
+
raise
|
| 138 |
+
fallback = "MPS" if MPS_READY else "CPU"
|
| 139 |
+
print(f"CUDA unavailable; restarting on {fallback}: {error}")
|
| 140 |
+
gr.Info(f"CUDA unavailable or quota exhausted. Running on {fallback}.")
|
| 141 |
+
if MPS_READY:
|
| 142 |
+
try:
|
| 143 |
+
yield from stream_denoising(seed, checkpoints, initial_image, "mps")
|
| 144 |
+
return
|
| 145 |
+
except (RuntimeError, NotImplementedError) as error:
|
| 146 |
+
message = str(error).lower()
|
| 147 |
+
if "mps" not in message and "metal" not in message:
|
| 148 |
+
raise
|
| 149 |
+
print(f"MPS unavailable; restarting on CPU: {error}")
|
| 150 |
+
gr.Info("MPS unavailable. Running on CPU.")
|
| 151 |
+
yield from stream_denoising(seed, checkpoints, initial_image, "cpu")
|
| 152 |
+
|
| 153 |
+
|
| 154 |
# Reuse the original page's palette, typography, header, and panel styling.
|
| 155 |
PAGE_CSS = (ROOT / "styles.css").read_text() + """
|
| 156 |
.gradio-container { max-width:1440px !important; padding:0 6% !important;
|
generate.py
CHANGED
|
@@ -11,7 +11,11 @@ ROOT = Path(__file__).resolve().parent
|
|
| 11 |
MODEL_ID = "google/ddpm-celebahq-256"
|
| 12 |
CHECKPOINTS = (0, 50, 100, 200, 350, 500, 700, 1000)
|
| 13 |
|
|
|
|
|
|
|
| 14 |
_MODEL_LOCK = threading.Lock()
|
|
|
|
|
|
|
| 15 |
_PIPELINE = None
|
| 16 |
_TORCH = None
|
| 17 |
_DEVICE = None
|
|
@@ -21,7 +25,7 @@ def load_model(device=None):
|
|
| 21 |
"""Load and cache the diffusion pipeline, returning it and its device."""
|
| 22 |
global _PIPELINE, _TORCH, _DEVICE
|
| 23 |
|
| 24 |
-
|
| 25 |
try:
|
| 26 |
import torch
|
| 27 |
from diffusers import DDPMPipeline
|
|
@@ -32,23 +36,24 @@ def load_model(device=None):
|
|
| 32 |
f"Import detail: {error}"
|
| 33 |
) from error
|
| 34 |
|
| 35 |
-
if device is
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
_TORCH = torch
|
| 48 |
_DEVICE = device
|
| 49 |
-
_PIPELINE
|
| 50 |
-
|
| 51 |
-
return _PIPELINE, _TORCH, _DEVICE
|
| 52 |
|
| 53 |
|
| 54 |
def is_model_loaded():
|
|
@@ -62,14 +67,17 @@ def sample_to_image(sample):
|
|
| 62 |
return Image.fromarray(image, mode="RGB")
|
| 63 |
|
| 64 |
|
| 65 |
-
def generate_images(seed, checkpoints=CHECKPOINTS, initial_image=None):
|
| 66 |
"""Yield every remaining-step count and an image at requested checkpoints."""
|
| 67 |
with _MODEL_LOCK:
|
| 68 |
-
pipeline, torch, device = load_model()
|
| 69 |
scheduler = pipeline.scheduler
|
| 70 |
training_steps = scheduler.config.num_train_timesteps
|
| 71 |
scheduler.set_timesteps(training_steps, device=device)
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
unet = pipeline.unet
|
| 74 |
sample_size = unet.config.sample_size
|
| 75 |
if isinstance(sample_size, (tuple, list)):
|
|
@@ -80,9 +88,9 @@ def generate_images(seed, checkpoints=CHECKPOINTS, initial_image=None):
|
|
| 80 |
sample = torch.randn(
|
| 81 |
(1, unet.config.in_channels, height, width),
|
| 82 |
generator=generator,
|
| 83 |
-
device=
|
| 84 |
dtype=unet.dtype,
|
| 85 |
-
) * scheduler.init_noise_sigma
|
| 86 |
else:
|
| 87 |
import numpy as np
|
| 88 |
|
|
|
|
| 11 |
MODEL_ID = "google/ddpm-celebahq-256"
|
| 12 |
CHECKPOINTS = (0, 50, 100, 200, 350, 500, 700, 1000)
|
| 13 |
|
| 14 |
+
# Gradio may resume a generator on a different worker thread between yields.
|
| 15 |
+
# A primitive Lock can be released there; RLock requires the acquiring thread.
|
| 16 |
_MODEL_LOCK = threading.Lock()
|
| 17 |
+
_CACHE_LOCK = threading.Lock()
|
| 18 |
+
_PIPELINES = {}
|
| 19 |
_PIPELINE = None
|
| 20 |
_TORCH = None
|
| 21 |
_DEVICE = None
|
|
|
|
| 25 |
"""Load and cache the diffusion pipeline, returning it and its device."""
|
| 26 |
global _PIPELINE, _TORCH, _DEVICE
|
| 27 |
|
| 28 |
+
with _CACHE_LOCK:
|
| 29 |
try:
|
| 30 |
import torch
|
| 31 |
from diffusers import DDPMPipeline
|
|
|
|
| 36 |
f"Import detail: {error}"
|
| 37 |
) from error
|
| 38 |
|
| 39 |
+
if device is None:
|
| 40 |
+
if torch.cuda.is_available():
|
| 41 |
+
device = "cuda"
|
| 42 |
+
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
| 43 |
+
device = "mps"
|
| 44 |
+
else:
|
| 45 |
+
device = "cpu"
|
| 46 |
+
device = str(device)
|
| 47 |
+
if device not in _PIPELINES:
|
| 48 |
+
print(f"Loading model on device: {device}")
|
| 49 |
+
pipeline = DDPMPipeline.from_pretrained(
|
| 50 |
+
MODEL_ID, cache_dir=Path(os.environ.get("MODEL_CACHE_DIR", ROOT / "models")))
|
| 51 |
+
pipeline.to(device)
|
| 52 |
+
_PIPELINES[device] = pipeline
|
| 53 |
+
_PIPELINE = _PIPELINES[device]
|
| 54 |
_TORCH = torch
|
| 55 |
_DEVICE = device
|
| 56 |
+
return _PIPELINE, _TORCH, _DEVICE
|
|
|
|
|
|
|
| 57 |
|
| 58 |
|
| 59 |
def is_model_loaded():
|
|
|
|
| 67 |
return Image.fromarray(image, mode="RGB")
|
| 68 |
|
| 69 |
|
| 70 |
+
def generate_images(seed, checkpoints=CHECKPOINTS, initial_image=None, device=None):
|
| 71 |
"""Yield every remaining-step count and an image at requested checkpoints."""
|
| 72 |
with _MODEL_LOCK:
|
| 73 |
+
pipeline, torch, device = load_model(device=device)
|
| 74 |
scheduler = pipeline.scheduler
|
| 75 |
training_steps = scheduler.config.num_train_timesteps
|
| 76 |
scheduler.set_timesteps(training_steps, device=device)
|
| 77 |
+
# MPS has no torch.Generator implementation. Keep its random stream
|
| 78 |
+
# on CPU; Diffusers also accepts this generator for scheduler noise.
|
| 79 |
+
random_device = "cpu" if device == "mps" else device
|
| 80 |
+
generator = torch.Generator(device=random_device).manual_seed(seed)
|
| 81 |
unet = pipeline.unet
|
| 82 |
sample_size = unet.config.sample_size
|
| 83 |
if isinstance(sample_size, (tuple, list)):
|
|
|
|
| 88 |
sample = torch.randn(
|
| 89 |
(1, unet.config.in_channels, height, width),
|
| 90 |
generator=generator,
|
| 91 |
+
device=random_device,
|
| 92 |
dtype=unet.dtype,
|
| 93 |
+
).to(device) * scheduler.init_noise_sigma
|
| 94 |
else:
|
| 95 |
import numpy as np
|
| 96 |
|