Spaces:
Runtime error
fix(deploy): switch to CPU inference for ZeroGPU compatibility
Browse filesZeroGPU's CUDA emulation is PyTorch-specific — llama-cpp-python links
directly against libcudart.so.12 which does not exist in the container.
- Remove --extra-index-url cu124 (installs CPU-only llama-cpp-python)
- Set python_version to 3.12 (ZeroGPU only supports 3.10/3.12)
- Detect SPACE_ID to force n_gpu_layers=0 on HF Spaces
- Remove @spaces.GPU from text-only LLM functions
- Keep @spaces.GPU on detection functions (PyTorch YOLO benefits)
- Add libopenblas-dev for CPU BLAS acceleration
- Remove Python 3.13 unraisablehook patch (no longer needed)
- Update README with local GPU install instructions
All hackathon badges preserved: Off the Grid, Llama Champion, Tiny Titan.
See docs/18-zerogpu-cpu-inference.md for full ADR.
Co-authored-by: Cursor <cursoragent@cursor.com>
- README.md +9 -5
- app.py +0 -23
- packages.txt +1 -0
- requirements.txt +0 -1
- src/model_loader.py +15 -7
|
@@ -5,7 +5,7 @@ colorFrom: green
|
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.17.3
|
| 8 |
-
python_version: "3.
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
|
@@ -61,16 +61,20 @@ Total parameters: **4 billion** — fits comfortably on a T4 GPU with Q4 quantiz
|
|
| 61 |
|
| 62 |
### On HuggingFace Spaces
|
| 63 |
|
| 64 |
-
The app runs on
|
| 65 |
-
1. Click **"Obtener Dataset"** to load a sample wardrobe from a HuggingFace dataset (takes ~
|
| 66 |
2. Or upload your own clothes photos in the **Captura** tab.
|
| 67 |
3. Explore combinations in **Combina** and ask questions in **Pregunta**.
|
| 68 |
|
| 69 |
-
### Local Development
|
| 70 |
|
| 71 |
```bash
|
| 72 |
cd packages/wardrobe-us
|
| 73 |
python -m venv .venv && source .venv/bin/activate
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
pip install -r requirements.txt
|
| 75 |
|
| 76 |
# Custom minimal frontend (default — gr.Server + Alpine.js):
|
|
@@ -80,7 +84,7 @@ python app.py
|
|
| 80 |
python app.py --default
|
| 81 |
```
|
| 82 |
|
| 83 |
-
Requires a CUDA GPU with at least 8GB VRAM. Set `HF_TOKEN` in `.env` for model downloads.
|
| 84 |
|
| 85 |
The default mode (`--ui`) uses `gradio.Server` with a vanilla HTML/CSS/JS + Alpine.js interface designed for non-technical users. Pass `--default` for the full Gradio Blocks UI with all advanced features (manual bounding box annotation, detection backend settings, etc.).
|
| 86 |
|
|
|
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.17.3
|
| 8 |
+
python_version: "3.12"
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
|
|
|
| 61 |
|
| 62 |
### On HuggingFace Spaces
|
| 63 |
|
| 64 |
+
The app runs on ZeroGPU with CPU-based LLM inference (llama.cpp). On first use:
|
| 65 |
+
1. Click **"Obtener Dataset"** to load a sample wardrobe from a HuggingFace dataset (takes ~5-10 minutes on CPU).
|
| 66 |
2. Or upload your own clothes photos in the **Captura** tab.
|
| 67 |
3. Explore combinations in **Combina** and ask questions in **Pregunta**.
|
| 68 |
|
| 69 |
+
### Local Development (GPU accelerated)
|
| 70 |
|
| 71 |
```bash
|
| 72 |
cd packages/wardrobe-us
|
| 73 |
python -m venv .venv && source .venv/bin/activate
|
| 74 |
+
|
| 75 |
+
# Install with CUDA 12.4 GPU acceleration (recommended):
|
| 76 |
+
pip install llama-cpp-python \
|
| 77 |
+
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124
|
| 78 |
pip install -r requirements.txt
|
| 79 |
|
| 80 |
# Custom minimal frontend (default — gr.Server + Alpine.js):
|
|
|
|
| 84 |
python app.py --default
|
| 85 |
```
|
| 86 |
|
| 87 |
+
Requires a CUDA GPU with at least 8GB VRAM for GPU mode. Without CUDA, the app falls back to CPU inference automatically. Set `HF_TOKEN` in `.env` for model downloads.
|
| 88 |
|
| 89 |
The default mode (`--ui`) uses `gradio.Server` with a vanilla HTML/CSS/JS + Alpine.js interface designed for non-technical users. Pass `--default` for the full Gradio Blocks UI with all advanced features (manual bounding box annotation, detection backend settings, etc.).
|
| 90 |
|
|
@@ -9,24 +9,8 @@ Built for the Build Small Hackathon (HuggingFace x Gradio, June 2026).
|
|
| 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")
|
|
@@ -146,7 +130,6 @@ def auto_detect(annotation, mode, progress=gr.Progress(track_tqdm=False)):
|
|
| 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 |
|
|
@@ -453,7 +436,6 @@ def clear_all():
|
|
| 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,7 +478,6 @@ def _get_combo_display(combo: dict | None) -> tuple[str | None, str | None, str,
|
|
| 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,7 +833,6 @@ def _build_custom_server():
|
|
| 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) -> dict:
|
| 857 |
if isinstance(image_path, dict):
|
| 858 |
image_path = image_path.get("path") or image_path.get("url", "")
|
|
@@ -865,7 +845,6 @@ def _build_custom_server():
|
|
| 865 |
return {"garments": added, "count": len(added)}
|
| 866 |
|
| 867 |
@server.api(name="get_combinations")
|
| 868 |
-
@spaces.GPU(duration=60)
|
| 869 |
def api_get_combinations(context: str = "") -> dict:
|
| 870 |
combos = generate_combinations()
|
| 871 |
if not combos:
|
|
@@ -890,14 +869,12 @@ def _build_custom_server():
|
|
| 890 |
return {"status": "ok", "liked": liked}
|
| 891 |
|
| 892 |
@server.api(name="ask_question")
|
| 893 |
-
@spaces.GPU(duration=90)
|
| 894 |
def api_ask_question(question: str) -> str:
|
| 895 |
if not question or not question.strip():
|
| 896 |
return "Please ask a question about your wardrobe."
|
| 897 |
return ask(question.strip())
|
| 898 |
|
| 899 |
@server.api(name="load_dataset")
|
| 900 |
-
@spaces.GPU(duration=300)
|
| 901 |
def api_load_dataset(dataset_key: str) -> dict:
|
| 902 |
from datasets import load_dataset as hf_load
|
| 903 |
|
|
|
|
| 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")
|
|
|
|
| 130 |
return _format_results(added)
|
| 131 |
|
| 132 |
|
|
|
|
| 133 |
def process_manual_selection(annotation, progress=gr.Progress(track_tqdm=False)):
|
| 134 |
"""Process manually drawn bounding boxes from the annotator.
|
| 135 |
|
|
|
|
| 436 |
# Chat handlers
|
| 437 |
# ---------------------------------------------------------------------------
|
| 438 |
|
|
|
|
| 439 |
def chat_respond(message, history):
|
| 440 |
"""Handle chat messages with streaming responses."""
|
| 441 |
if not message:
|
|
|
|
| 478 |
return top_img, bottom_img, top_text, bottom_text
|
| 479 |
|
| 480 |
|
|
|
|
| 481 |
def init_combinations(state, context):
|
| 482 |
"""Initialize or refresh the combination queue, optionally ranked by context."""
|
| 483 |
combos = generate_combinations()
|
|
|
|
| 833 |
return {"garments": catalog, "count": len(catalog)}
|
| 834 |
|
| 835 |
@server.api(name="add_photo")
|
|
|
|
| 836 |
def api_add_photo(image_path) -> dict:
|
| 837 |
if isinstance(image_path, dict):
|
| 838 |
image_path = image_path.get("path") or image_path.get("url", "")
|
|
|
|
| 845 |
return {"garments": added, "count": len(added)}
|
| 846 |
|
| 847 |
@server.api(name="get_combinations")
|
|
|
|
| 848 |
def api_get_combinations(context: str = "") -> dict:
|
| 849 |
combos = generate_combinations()
|
| 850 |
if not combos:
|
|
|
|
| 869 |
return {"status": "ok", "liked": liked}
|
| 870 |
|
| 871 |
@server.api(name="ask_question")
|
|
|
|
| 872 |
def api_ask_question(question: str) -> str:
|
| 873 |
if not question or not question.strip():
|
| 874 |
return "Please ask a question about your wardrobe."
|
| 875 |
return ask(question.strip())
|
| 876 |
|
| 877 |
@server.api(name="load_dataset")
|
|
|
|
| 878 |
def api_load_dataset(dataset_key: str) -> dict:
|
| 879 |
from datasets import load_dataset as hf_load
|
| 880 |
|
|
@@ -1,2 +1,3 @@
|
|
| 1 |
cmake
|
| 2 |
build-essential
|
|
|
|
|
|
| 1 |
cmake
|
| 2 |
build-essential
|
| 3 |
+
libopenblas-dev
|
|
@@ -1,4 +1,3 @@
|
|
| 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
|
|
|
|
|
|
|
| 1 |
spaces
|
| 2 |
gradio==6.17.3
|
| 3 |
llama-cpp-python>=0.3.28
|
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
"""Singleton model loader
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
"""
|
| 9 |
|
| 10 |
import gc
|
|
@@ -96,7 +96,15 @@ class _ModelManager:
|
|
| 96 |
|
| 97 |
@staticmethod
|
| 98 |
def _detect_gpu_layers() -> int:
|
| 99 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
if os.environ.get("CUDA_VISIBLE_DEVICES") == "":
|
| 101 |
return 0
|
| 102 |
try:
|
|
|
|
| 1 |
+
"""Singleton model loader for GGUF models.
|
| 2 |
|
| 3 |
+
Keeps one model loaded at a time. Supports dual-mode deployment:
|
| 4 |
+
- HF Spaces (ZeroGPU): CPU-only inference (n_gpu_layers=0) because
|
| 5 |
+
ZeroGPU's CUDA emulation is PyTorch-specific and llama.cpp links
|
| 6 |
+
against the system CUDA runtime which is not available.
|
| 7 |
+
- Local: Full GPU offload via CUDA when libcudart.so.12 is present.
|
| 8 |
"""
|
| 9 |
|
| 10 |
import gc
|
|
|
|
| 96 |
|
| 97 |
@staticmethod
|
| 98 |
def _detect_gpu_layers() -> int:
|
| 99 |
+
"""Detect whether to offload layers to GPU.
|
| 100 |
+
|
| 101 |
+
On HF Spaces the CUDA runtime is unavailable to llama.cpp
|
| 102 |
+
(ZeroGPU only exposes CUDA through PyTorch emulation).
|
| 103 |
+
Locally, probe for libcudart.so.12 to confirm real CUDA.
|
| 104 |
+
"""
|
| 105 |
+
if os.environ.get("SPACE_ID"):
|
| 106 |
+
logger.info("Running on HF Spaces — using CPU inference")
|
| 107 |
+
return 0
|
| 108 |
if os.environ.get("CUDA_VISIBLE_DEVICES") == "":
|
| 109 |
return 0
|
| 110 |
try:
|