vulus98 commited on
Commit
aeea5fe
Β·
1 Parent(s): db91e6d

Fix ZeroGPU startup: disable Xet, run inference behind @spaces.GPU

Browse files

The 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.

Files changed (4) hide show
  1. README.md +2 -1
  2. app.py +34 -3
  3. requirements.txt +3 -0
  4. src/utils/scene_classifier.py +31 -10
README.md CHANGED
@@ -23,7 +23,8 @@ tags:
23
  - arxiv:2605.26368
24
  preload_from_hub:
25
  - prs-eth/PaGeR
26
- suggested_hardware: a10g-large
 
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
 
app.py CHANGED
@@ -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
- if torch.cuda.is_available():
82
- torch.cuda.empty_cache()
83
- torch.cuda.ipc_collect()
 
 
 
 
 
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.
requirements.txt CHANGED
@@ -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
src/utils/scene_classifier.py CHANGED
@@ -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
- with torch.inference_mode():
96
- centroids = {}
97
- for key, prompts in (("indoor", indoor_prompts), ("outdoor", outdoor_prompts)):
98
- toks = tokenizer(list(prompts)).to(self.device)
99
- feats = F.normalize(self.model.encode_text(toks), dim=-1)
100
- centroids[key] = F.normalize(feats.mean(dim=0, keepdim=True), dim=-1)
101
- self.text_indoor = centroids["indoor"] # (1, D)
102
- self.text_outdoor = centroids["outdoor"] # (1, D)
 
 
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)