nanye commited on
Commit
d4ec601
·
1 Parent(s): 654f436

try device in the order of gpu/mps/cpu

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. app.py +62 -9
  3. 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, loads the model at startup, and places it on CUDA when `SPACES_ZERO_GPU` is enabled. GPU sampling runs inside a decorated 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
 
 
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
- load_model(device="cuda" if ZERO_GPU else None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- @spaces.GPU(duration=int(os.environ.get("GPU_DURATION_SECONDS", "120")))
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
- for step, frame in generate_images(seed, checkpoints, initial_image):
 
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
- if _PIPELINE is None:
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 not None:
36
- pass
37
- elif torch.cuda.is_available():
38
- device = "cuda"
39
- elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
40
- device = "mps"
41
- else:
42
- device = "cpu"
43
-
44
- print(f"Using device: {device}")
45
- pipeline = DDPMPipeline.from_pretrained(MODEL_ID, cache_dir=Path(os.environ.get("MODEL_CACHE_DIR", ROOT / "models")))
46
- pipeline.to(device)
 
 
 
47
  _TORCH = torch
48
  _DEVICE = device
49
- _PIPELINE = 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
- generator = torch.Generator(device=device).manual_seed(seed)
 
 
 
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=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