Spaces:
Running on Zero
Fix ZeroGPU startup: disable Xet, run inference behind @spaces.GPU
Browse filesThe Space moved to ZeroGPU (zero-a10g), which broke startup in three ways:
- The HF Xet downloader can't write its cache inside the ZeroGPU sandbox
("Permission denied (os error 13)"), crashing when open_clip pulled the
CLIP classifier weights at runtime. Force the classic HTTP downloader
(HF_HUB_DISABLE_XET=1, set before huggingface_hub is imported) and also
preload the CLIP repo at build time.
- GPU work must run inside a @spaces.GPU window. Wrap _run_inference in
@spaces.GPU(duration=120), import the spaces scheduler (no-op fallback for
local/non-ZeroGPU dev), and guard the CUDA cleanup for when no device is
attached.
- The CLIP classifier ran a text-encoder forward pass in its constructor; on
ZeroGPU a forward at import time has no GPU. Defer the centroid computation
to first use (inside the GPU window) and keep only the weight load eager.
Also align suggested_hardware with the deployed zero-a10g.
- README.md +2 -1
- app.py +34 -3
- requirements.txt +3 -0
- src/utils/scene_classifier.py +31 -10
|
@@ -23,7 +23,8 @@ tags:
|
|
| 23 |
- arxiv:2605.26368
|
| 24 |
preload_from_hub:
|
| 25 |
- prs-eth/PaGeR
|
| 26 |
-
|
|
|
|
| 27 |
short_description: Panorama Geometry Reconstruction
|
| 28 |
---
|
| 29 |
|
|
|
|
| 23 |
- arxiv:2605.26368
|
| 24 |
preload_from_hub:
|
| 25 |
- prs-eth/PaGeR
|
| 26 |
+
- timm/vit_base_patch32_clip_224.openai
|
| 27 |
+
suggested_hardware: zero-a10g
|
| 28 |
short_description: Panorama Geometry Reconstruction
|
| 29 |
---
|
| 30 |
|
|
@@ -18,11 +18,36 @@ from __future__ import annotations
|
|
| 18 |
|
| 19 |
import argparse
|
| 20 |
import gc
|
|
|
|
| 21 |
import sys
|
| 22 |
from io import BytesIO
|
| 23 |
from pathlib import Path
|
| 24 |
from tempfile import NamedTemporaryFile
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
# Expose the vendored ``depth_anything_3`` package at top-level so its internal
|
| 27 |
# absolute imports resolve even when this script is run without an editable
|
| 28 |
# install (e.g. on HuggingFace Spaces, where ``pip install -e .`` is not run).
|
|
@@ -78,9 +103,14 @@ def parse_args() -> argparse.Namespace:
|
|
| 78 |
|
| 79 |
|
| 80 |
def _release_cuda_memory() -> None:
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
gc.collect()
|
| 85 |
|
| 86 |
|
|
@@ -103,6 +133,7 @@ def _resolve_scene(mode: str, rgb_cubemap_centred: torch.Tensor) -> tuple[str, s
|
|
| 103 |
return scene, f"**Scale head:** Auto β {scene}"
|
| 104 |
|
| 105 |
|
|
|
|
| 106 |
def _run_inference(input_rgb: np.ndarray, mode: str):
|
| 107 |
img = torch.from_numpy(input_rgb).permute(2, 0, 1).to(torch.float32) / 255.0
|
| 108 |
img = img * 2.0 - 1.0 # ImageNet-style centred input for the backbone.
|
|
|
|
| 18 |
|
| 19 |
import argparse
|
| 20 |
import gc
|
| 21 |
+
import os
|
| 22 |
import sys
|
| 23 |
from io import BytesIO
|
| 24 |
from pathlib import Path
|
| 25 |
from tempfile import NamedTemporaryFile
|
| 26 |
|
| 27 |
+
# HuggingFace's Xet download backend can't write its chunk cache inside the
|
| 28 |
+
# ZeroGPU sandbox (``Permission denied (os error 13)``), which crashed startup
|
| 29 |
+
# when open_clip pulled the CLIP weights at runtime. Force the classic HTTP
|
| 30 |
+
# downloader, which writes to the normal (writable) hub cache. Must be set
|
| 31 |
+
# before ``huggingface_hub`` is first imported, hence up here.
|
| 32 |
+
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
|
| 33 |
+
|
| 34 |
+
# ``spaces`` provides the ZeroGPU scheduler: GPU work must run inside a
|
| 35 |
+
# ``@spaces.GPU`` function so the runtime attaches a GPU for its duration.
|
| 36 |
+
# Import it before torch. Off ZeroGPU (local dev) the package is absent, so
|
| 37 |
+
# fall back to a no-op decorator that supports both ``@spaces.GPU`` and
|
| 38 |
+
# ``@spaces.GPU(...)``.
|
| 39 |
+
try:
|
| 40 |
+
import spaces
|
| 41 |
+
except ImportError:
|
| 42 |
+
class _SpacesStub:
|
| 43 |
+
@staticmethod
|
| 44 |
+
def GPU(*args, **kwargs):
|
| 45 |
+
if len(args) == 1 and callable(args[0]) and not kwargs:
|
| 46 |
+
return args[0]
|
| 47 |
+
return lambda fn: fn
|
| 48 |
+
|
| 49 |
+
spaces = _SpacesStub()
|
| 50 |
+
|
| 51 |
# Expose the vendored ``depth_anything_3`` package at top-level so its internal
|
| 52 |
# absolute imports resolve even when this script is run without an editable
|
| 53 |
# install (e.g. on HuggingFace Spaces, where ``pip install -e .`` is not run).
|
|
|
|
| 103 |
|
| 104 |
|
| 105 |
def _release_cuda_memory() -> None:
|
| 106 |
+
# On ZeroGPU this can run outside a GPU window (no device attached), where
|
| 107 |
+
# the cache calls aren't usable β swallow that and just collect on the host.
|
| 108 |
+
try:
|
| 109 |
+
if torch.cuda.is_available():
|
| 110 |
+
torch.cuda.empty_cache()
|
| 111 |
+
torch.cuda.ipc_collect()
|
| 112 |
+
except Exception:
|
| 113 |
+
pass
|
| 114 |
gc.collect()
|
| 115 |
|
| 116 |
|
|
|
|
| 133 |
return scene, f"**Scale head:** Auto β {scene}"
|
| 134 |
|
| 135 |
|
| 136 |
+
@spaces.GPU(duration=120)
|
| 137 |
def _run_inference(input_rgb: np.ndarray, mode: str):
|
| 138 |
img = torch.from_numpy(input_rgb).permute(2, 0, 1).to(torch.float32) / 255.0
|
| 139 |
img = img * 2.0 - 1.0 # ImageNet-style centred input for the backbone.
|
|
@@ -19,3 +19,6 @@ pytorch360convert
|
|
| 19 |
matplotlib
|
| 20 |
trimesh
|
| 21 |
gradio>=5
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
matplotlib
|
| 20 |
trimesh
|
| 21 |
gradio>=5
|
| 22 |
+
|
| 23 |
+
# ZeroGPU scheduler (@spaces.GPU); pre-installed on ZeroGPU hardware.
|
| 24 |
+
spaces
|
|
@@ -90,20 +90,40 @@ class IndoorOutdoorClassifier:
|
|
| 90 |
force_quick_gelu=(pretrained == "openai"),
|
| 91 |
)
|
| 92 |
self.model = model.eval()
|
| 93 |
-
tokenizer = open_clip.get_tokenizer(model_name)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
self.
|
| 102 |
-
self.
|
|
|
|
|
|
|
| 103 |
|
| 104 |
# Image-side normalisation buffers (kept on device for fast inference).
|
| 105 |
self.register_clip_norm(self.device)
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
def register_clip_norm(self, device: torch.device) -> None:
|
| 108 |
self._clip_mean = torch.tensor(_CLIP_MEAN, device=device).view(1, 3, 1, 1)
|
| 109 |
self._clip_std = torch.tensor(_CLIP_STD, device=device).view(1, 3, 1, 1)
|
|
@@ -126,6 +146,7 @@ class IndoorOutdoorClassifier:
|
|
| 126 |
raise ValueError(
|
| 127 |
f"Expected cubemap of shape (6, 3, F, F); got {tuple(cubemap_01.shape)}"
|
| 128 |
)
|
|
|
|
| 129 |
eq = cubemap_01[:4].to(self.device).clamp(0, 1)
|
| 130 |
eq = F.interpolate(eq, size=(224, 224), mode="bilinear",
|
| 131 |
align_corners=False, antialias=True)
|
|
|
|
| 90 |
force_quick_gelu=(pretrained == "openai"),
|
| 91 |
)
|
| 92 |
self.model = model.eval()
|
| 93 |
+
self.tokenizer = open_clip.get_tokenizer(model_name)
|
| 94 |
+
|
| 95 |
+
# The text centroids are built lazily on first ``classify`` call -- see
|
| 96 |
+
# ``_ensure_centroids``. Building them here would run a CLIP text-encoder
|
| 97 |
+
# forward pass at construction time, which breaks on HF ZeroGPU: a GPU
|
| 98 |
+
# is only attached inside a ``@spaces.GPU`` window, so a forward run at
|
| 99 |
+
# import/startup has no device. Loading the weights (above) is fine;
|
| 100 |
+
# only the forward must wait for a GPU window.
|
| 101 |
+
self._indoor_prompts = tuple(indoor_prompts)
|
| 102 |
+
self._outdoor_prompts = tuple(outdoor_prompts)
|
| 103 |
+
self.text_indoor = None # (1, D), filled lazily
|
| 104 |
+
self.text_outdoor = None # (1, D), filled lazily
|
| 105 |
|
| 106 |
# Image-side normalisation buffers (kept on device for fast inference).
|
| 107 |
self.register_clip_norm(self.device)
|
| 108 |
|
| 109 |
+
@torch.inference_mode()
|
| 110 |
+
def _ensure_centroids(self) -> None:
|
| 111 |
+
"""Build the indoor/outdoor text centroids on first use (idempotent).
|
| 112 |
+
|
| 113 |
+
Each centroid is the L2-normalised mean of its prompts' L2-normalised
|
| 114 |
+
CLIP text embeddings. Deferred out of ``__init__`` so the forward pass
|
| 115 |
+
runs inside the caller's GPU window (see the note there)."""
|
| 116 |
+
if self.text_indoor is not None:
|
| 117 |
+
return
|
| 118 |
+
centroids = {}
|
| 119 |
+
for key, prompts in (("indoor", self._indoor_prompts),
|
| 120 |
+
("outdoor", self._outdoor_prompts)):
|
| 121 |
+
toks = self.tokenizer(list(prompts)).to(self.device)
|
| 122 |
+
feats = F.normalize(self.model.encode_text(toks), dim=-1)
|
| 123 |
+
centroids[key] = F.normalize(feats.mean(dim=0, keepdim=True), dim=-1)
|
| 124 |
+
self.text_indoor = centroids["indoor"] # (1, D)
|
| 125 |
+
self.text_outdoor = centroids["outdoor"] # (1, D)
|
| 126 |
+
|
| 127 |
def register_clip_norm(self, device: torch.device) -> None:
|
| 128 |
self._clip_mean = torch.tensor(_CLIP_MEAN, device=device).view(1, 3, 1, 1)
|
| 129 |
self._clip_std = torch.tensor(_CLIP_STD, device=device).view(1, 3, 1, 1)
|
|
|
|
| 146 |
raise ValueError(
|
| 147 |
f"Expected cubemap of shape (6, 3, F, F); got {tuple(cubemap_01.shape)}"
|
| 148 |
)
|
| 149 |
+
self._ensure_centroids()
|
| 150 |
eq = cubemap_01[:4].to(self.device).clamp(0, 1)
|
| 151 |
eq = F.interpolate(eq, size=(224, 224), mode="bilinear",
|
| 152 |
align_corners=False, antialias=True)
|