Ox1 commited on
Commit
e7af96c
·
1 Parent(s): 82204a5

fix (hf): use gpu from hf space

Browse files
Files changed (4) hide show
  1. README.md +1 -1
  2. app.py +38 -0
  3. requirements.txt +1 -0
  4. src/model_loader.py +24 -3
README.md CHANGED
@@ -9,7 +9,7 @@ python_version: "3.13"
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
- hardware: t4-small
13
  short_description: AI wardrobe. catalog, combine and ask about your clothes
14
  ---
15
 
 
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
+ hardware: zero-gpu
13
  short_description: AI wardrobe. catalog, combine and ask about your clothes
14
  ---
15
 
app.py CHANGED
@@ -9,8 +9,24 @@ Built for the Build Small Hackathon (HuggingFace x Gradio, June 2026).
9
  import io
10
  import logging
11
  import os
 
12
  from pathlib import Path
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  from dotenv import load_dotenv
15
 
16
  load_dotenv(Path(__file__).resolve().parent / ".env")
@@ -38,6 +54,19 @@ from src.detector import detect_garments as detect_boxes, crop_garments, list_av
38
  from src import settings
39
  from gradio_image_annotation import image_annotator
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  os.environ.setdefault("CUDA_VISIBLE_DEVICES", "0")
42
  logging.basicConfig(level=logging.INFO, format="%(name)s | %(message)s")
43
 
@@ -79,6 +108,7 @@ def _format_results(added: list[dict]) -> tuple[str, list]:
79
  return status, table_data
80
 
81
 
 
82
  def auto_detect(annotation, mode, progress=gr.Progress(track_tqdm=False)):
83
  """Run automatic detection on the uploaded image.
84
 
@@ -116,6 +146,7 @@ def auto_detect(annotation, mode, progress=gr.Progress(track_tqdm=False)):
116
  return _format_results(added)
117
 
118
 
 
119
  def process_manual_selection(annotation, progress=gr.Progress(track_tqdm=False)):
120
  """Process manually drawn bounding boxes from the annotator.
121
 
@@ -185,6 +216,7 @@ SAMPLE_DATASETS = [
185
  TARGET_GARMENTS = 50
186
 
187
 
 
188
  def obtener_dataset(dataset_key: str):
189
  """Download a HF dataset and process garments in-process with real-time UI updates.
190
 
@@ -421,6 +453,7 @@ def clear_all():
421
  # Chat handlers
422
  # ---------------------------------------------------------------------------
423
 
 
424
  def chat_respond(message, history):
425
  """Handle chat messages with streaming responses."""
426
  if not message:
@@ -463,6 +496,7 @@ def _get_combo_display(combo: dict | None) -> tuple[str | None, str | None, str,
463
  return top_img, bottom_img, top_text, bottom_text
464
 
465
 
 
466
  def init_combinations(state, context):
467
  """Initialize or refresh the combination queue, optionally ranked by context."""
468
  combos = generate_combinations()
@@ -818,6 +852,7 @@ def _build_custom_server():
818
  return {"garments": catalog, "count": len(catalog)}
819
 
820
  @server.api(name="add_photo")
 
821
  def api_add_photo(image_path: str) -> dict:
822
  results = extract_garments(image_path)
823
  if not results:
@@ -828,6 +863,7 @@ def _build_custom_server():
828
  return {"garments": added, "count": len(added)}
829
 
830
  @server.api(name="get_combinations")
 
831
  def api_get_combinations(context: str = "") -> dict:
832
  combos = generate_combinations()
833
  if not combos:
@@ -852,12 +888,14 @@ def _build_custom_server():
852
  return {"status": "ok", "liked": liked}
853
 
854
  @server.api(name="ask_question")
 
855
  def api_ask_question(question: str) -> str:
856
  if not question or not question.strip():
857
  return "Please ask a question about your wardrobe."
858
  return ask(question.strip())
859
 
860
  @server.api(name="load_dataset")
 
861
  def api_load_dataset(dataset_key: str) -> dict:
862
  from datasets import load_dataset as hf_load
863
 
 
9
  import io
10
  import logging
11
  import os
12
+ import sys
13
  from pathlib import Path
14
 
15
+ # Python 3.13 raises spurious ValueError during event-loop GC when httpx
16
+ # (used internally by Gradio) creates and discards temporary loops.
17
+ # Suppress the noise — it has no functional impact.
18
+ if sys.version_info >= (3, 13):
19
+ _original_unraisablehook = sys.unraisablehook
20
+
21
+ def _quiet_unraisablehook(unraisable):
22
+ if isinstance(unraisable.exc_value, ValueError) and "file descriptor" in str(
23
+ unraisable.exc_value
24
+ ):
25
+ return
26
+ _original_unraisablehook(unraisable)
27
+
28
+ sys.unraisablehook = _quiet_unraisablehook
29
+
30
  from dotenv import load_dotenv
31
 
32
  load_dotenv(Path(__file__).resolve().parent / ".env")
 
54
  from src import settings
55
  from gradio_image_annotation import image_annotator
56
 
57
+ try:
58
+ import spaces
59
+ except ImportError:
60
+ class _NoOpGPU:
61
+ """Fallback when not running on HF Spaces (local dev)."""
62
+ def __call__(self, fn=None, *, duration=None):
63
+ if fn is None:
64
+ return lambda f: f
65
+ return fn
66
+
67
+ class spaces: # type: ignore[no-redef]
68
+ GPU = _NoOpGPU()
69
+
70
  os.environ.setdefault("CUDA_VISIBLE_DEVICES", "0")
71
  logging.basicConfig(level=logging.INFO, format="%(name)s | %(message)s")
72
 
 
108
  return status, table_data
109
 
110
 
111
+ @spaces.GPU(duration=120)
112
  def auto_detect(annotation, mode, progress=gr.Progress(track_tqdm=False)):
113
  """Run automatic detection on the uploaded image.
114
 
 
146
  return _format_results(added)
147
 
148
 
149
+ @spaces.GPU(duration=120)
150
  def process_manual_selection(annotation, progress=gr.Progress(track_tqdm=False)):
151
  """Process manually drawn bounding boxes from the annotator.
152
 
 
216
  TARGET_GARMENTS = 50
217
 
218
 
219
+ @spaces.GPU(duration=300)
220
  def obtener_dataset(dataset_key: str):
221
  """Download a HF dataset and process garments in-process with real-time UI updates.
222
 
 
453
  # Chat handlers
454
  # ---------------------------------------------------------------------------
455
 
456
+ @spaces.GPU(duration=90)
457
  def chat_respond(message, history):
458
  """Handle chat messages with streaming responses."""
459
  if not message:
 
496
  return top_img, bottom_img, top_text, bottom_text
497
 
498
 
499
+ @spaces.GPU(duration=60)
500
  def init_combinations(state, context):
501
  """Initialize or refresh the combination queue, optionally ranked by context."""
502
  combos = generate_combinations()
 
852
  return {"garments": catalog, "count": len(catalog)}
853
 
854
  @server.api(name="add_photo")
855
+ @spaces.GPU(duration=120)
856
  def api_add_photo(image_path: str) -> dict:
857
  results = extract_garments(image_path)
858
  if not results:
 
863
  return {"garments": added, "count": len(added)}
864
 
865
  @server.api(name="get_combinations")
866
+ @spaces.GPU(duration=60)
867
  def api_get_combinations(context: str = "") -> dict:
868
  combos = generate_combinations()
869
  if not combos:
 
888
  return {"status": "ok", "liked": liked}
889
 
890
  @server.api(name="ask_question")
891
+ @spaces.GPU(duration=90)
892
  def api_ask_question(question: str) -> str:
893
  if not question or not question.strip():
894
  return "Please ask a question about your wardrobe."
895
  return ask(question.strip())
896
 
897
  @server.api(name="load_dataset")
898
+ @spaces.GPU(duration=300)
899
  def api_load_dataset(dataset_key: str) -> dict:
900
  from datasets import load_dataset as hf_load
901
 
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
  --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124
 
2
  gradio==6.17.3
3
  llama-cpp-python>=0.3.28
4
  huggingface-hub>=1.18.0
 
1
  --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124
2
+ spaces
3
  gradio==6.17.3
4
  llama-cpp-python>=0.3.28
5
  huggingface-hub>=1.18.0
src/model_loader.py CHANGED
@@ -1,11 +1,15 @@
1
  """Singleton model loader with VRAM management.
2
 
3
  Handles loading/unloading of GGUF models. Only one model is kept in
4
- memory at a time to fit within 8 GB VRAM.
 
 
 
5
  """
6
 
7
  import gc
8
  import logging
 
9
  from pathlib import Path
10
  from dataclasses import dataclass
11
 
@@ -90,6 +94,19 @@ class _ModelManager:
90
 
91
  return model_path, mmproj_path
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  def _is_same_model(self, config: ModelConfig) -> bool:
94
  if self._current_config is None:
95
  return False
@@ -119,12 +136,16 @@ class _ModelManager:
119
  from llama_cpp.llama_chat_format import Qwen25VLChatHandler
120
  chat_handler = Qwen25VLChatHandler(clip_model_path=str(mmproj_path))
121
 
122
- logger.info("Loading model: %s (handler: %s)", config.model_file, config.handler_type)
 
 
 
 
123
 
124
  self._llm = Llama(
125
  model_path=str(model_path),
126
  chat_handler=chat_handler,
127
- n_gpu_layers=-1,
128
  n_ctx=config.n_ctx,
129
  verbose=False,
130
  )
 
1
  """Singleton model loader with VRAM management.
2
 
3
  Handles loading/unloading of GGUF models. Only one model is kept in
4
+ memory at a time to fit within available VRAM.
5
+
6
+ Supports ZeroGPU (dynamic GPU allocation on HF Spaces) by detecting
7
+ CUDA availability at load time rather than import time.
8
  """
9
 
10
  import gc
11
  import logging
12
+ import os
13
  from pathlib import Path
14
  from dataclasses import dataclass
15
 
 
94
 
95
  return model_path, mmproj_path
96
 
97
+ @staticmethod
98
+ def _detect_gpu_layers() -> int:
99
+ """Return -1 (all layers on GPU) if CUDA is available, else 0 (CPU)."""
100
+ if os.environ.get("CUDA_VISIBLE_DEVICES") == "":
101
+ return 0
102
+ try:
103
+ import ctypes
104
+ ctypes.CDLL("libcudart.so.12")
105
+ return -1
106
+ except OSError:
107
+ logger.warning("CUDA runtime not found — running on CPU")
108
+ return 0
109
+
110
  def _is_same_model(self, config: ModelConfig) -> bool:
111
  if self._current_config is None:
112
  return False
 
136
  from llama_cpp.llama_chat_format import Qwen25VLChatHandler
137
  chat_handler = Qwen25VLChatHandler(clip_model_path=str(mmproj_path))
138
 
139
+ n_gpu_layers = self._detect_gpu_layers()
140
+ logger.info(
141
+ "Loading model: %s (handler: %s, gpu_layers: %s)",
142
+ config.model_file, config.handler_type, n_gpu_layers,
143
+ )
144
 
145
  self._llm = Llama(
146
  model_path=str(model_path),
147
  chat_handler=chat_handler,
148
+ n_gpu_layers=n_gpu_layers,
149
  n_ctx=config.n_ctx,
150
  verbose=False,
151
  )