RoyalityAi commited on
Commit
9ae8086
·
verified ·
1 Parent(s): 2cd10cc

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +133 -0
  2. README.md +179 -5
  3. app.py +329 -0
  4. requirements.txt +18 -0
Dockerfile ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+ # ---------------------------------------------------------------------------
3
+ # Dockerfile for a Hugging Face "Docker Space" that serves a GGUF model
4
+ # (empero-ai/Qwythos-9B-Claude-Mythos-5-1M-GGUF) through llama-cpp-python
5
+ # with a streaming Gradio chat UI.
6
+ #
7
+ # Design goals:
8
+ # - Small final image -> multi-stage build. All compilers / build tools
9
+ # (cmake, ninja, gcc) live ONLY in the builder stage. The runtime stage
10
+ # just installs the pre-built wheel.
11
+ # - Reliable build -> pin apt/pip behaviour (no interactive prompts,
12
+ # no cache dirs left behind, explicit CMake flags for llama.cpp so the
13
+ # build never silently falls back to something incompatible with the
14
+ # CPU the Space actually runs on).
15
+ # - CPU-only -> no CUDA/ROCm toolkits anywhere in the image.
16
+ # ---------------------------------------------------------------------------
17
+
18
+ # =========================== 1. Builder stage ===============================
19
+ FROM python:3.11-slim AS builder
20
+
21
+ # Build-time system dependencies for compiling llama-cpp-python's C++/CMake
22
+ # backend (llama.cpp). None of this ends up in the final image.
23
+ RUN apt-get update && apt-get install -y --no-install-recommends \
24
+ build-essential \
25
+ cmake \
26
+ ninja-build \
27
+ git \
28
+ ca-certificates \
29
+ && rm -rf /var/lib/apt/lists/*
30
+
31
+ # CMake flags for llama.cpp's CPU backend:
32
+ # - GGML_NATIVE=OFF : do NOT auto-detect the build machine's CPU flags.
33
+ # The Docker image is built on different hardware
34
+ # than it may eventually run on, so "native" builds
35
+ # can crash with "illegal instruction" on the actual
36
+ # Space runner. We instead opt in to a conservative,
37
+ # broadly-supported instruction set explicitly.
38
+ # - GGML_AVX2/FMA/F16C: virtually all HF CPU Space runners support these
39
+ # (modern x86_64 cloud CPUs). This gives good
40
+ # performance without the risk of AVX-512-only code
41
+ # paths.
42
+ # - CMAKE_BUILD_TYPE=Release + Ninja generator: faster, smaller, more
43
+ # reliable builds than the default Makefiles.
44
+ ENV CMAKE_ARGS="-DGGML_NATIVE=OFF -DGGML_AVX2=ON -DGGML_FMA=ON -DGGML_F16C=ON -DCMAKE_BUILD_TYPE=Release -GNinja"
45
+ ENV FORCE_CMAKE=1
46
+
47
+ WORKDIR /build
48
+
49
+ # Build a wheel for the latest stable llama-cpp-python (and its light
50
+ # transitive deps) instead of `pip install`-ing it directly. This lets the
51
+ # final runtime stage install from a local wheel with zero compilers
52
+ # present, which is both faster and smaller.
53
+ RUN pip install --no-cache-dir --upgrade pip wheel setuptools && \
54
+ pip wheel --no-cache-dir --wheel-dir /wheels "llama-cpp-python"
55
+
56
+ # ============================ 2. Runtime stage ================================
57
+ FROM python:3.11-slim AS runtime
58
+
59
+ LABEL maintainer="hf-space" \
60
+ description="Gradio chat UI serving empero-ai/Qwythos-9B-Claude-Mythos-5-1M-GGUF via llama-cpp-python"
61
+
62
+ # Runtime-only system dependencies:
63
+ # - libgomp1: OpenMP runtime required by llama.cpp's multithreaded kernels.
64
+ # - ca-certificates: needed for HTTPS downloads from the Hugging Face Hub.
65
+ # NOTE: no compilers, no cmake, no git here -> keeps the final image lean.
66
+ RUN apt-get update && apt-get install -y --no-install-recommends \
67
+ libgomp1 \
68
+ ca-certificates \
69
+ && rm -rf /var/lib/apt/lists/*
70
+
71
+ # Run as a non-root user (Hugging Face Spaces requirement/best practice).
72
+ RUN useradd --create-home --uid 1000 appuser
73
+ WORKDIR /app
74
+
75
+ # Install the pre-built llama-cpp-python wheel from the builder stage.
76
+ COPY --from=builder /wheels /wheels
77
+ COPY requirements.txt .
78
+
79
+ RUN pip install --no-cache-dir --upgrade pip && \
80
+ pip install --no-cache-dir /wheels/*.whl && \
81
+ pip install --no-cache-dir -r requirements.txt && \
82
+ rm -rf /wheels /root/.cache/pip ~/.cache/pip
83
+
84
+ # Application code
85
+ COPY app.py .
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Runtime configuration (all overridable as Space "Variables and secrets"
89
+ # without touching the Dockerfile). See README.md for the full list.
90
+ # ---------------------------------------------------------------------------
91
+ ENV \
92
+ # Where to look for the GGUF model on the Hub.
93
+ GGUF_REPO_ID="empero-ai/Qwythos-9B-Claude-Mythos-5-1M-GGUF" \
94
+ # Leave empty to auto-select; set to force an exact filename.
95
+ GGUF_FILENAME="" \
96
+ # Quantization to prefer when auto-selecting (falls back automatically
97
+ # if this exact quant isn't present in the repo).
98
+ PREFERRED_QUANT="Q4_K_M" \
99
+ # Local, persistent-within-container cache for downloaded model files
100
+ # and Hub metadata so restarts of a *running* Space don't re-download.
101
+ MODEL_CACHE_DIR="/data/models" \
102
+ HF_HOME="/data/hf_home" \
103
+ HF_HUB_ENABLE_HF_TRANSFER="0" \
104
+ # Context window (tokens). The model supports up to 1,048,576 via
105
+ # baked-in YaRN scaling, but free CPU Spaces cannot allocate that much
106
+ # KV-cache. Default is a safe value for a 16GB-RAM CPU Space; raise it
107
+ # via the Space's Variables UI if you have more headroom.
108
+ N_CTX="8192" \
109
+ # 0 = auto-detect available CPU cores.
110
+ N_THREADS="0" \
111
+ N_BATCH="256" \
112
+ MAX_NEW_TOKENS="1024" \
113
+ TEMPERATURE="0.6" \
114
+ TOP_P="0.95" \
115
+ TOP_K="20" \
116
+ REPEAT_PENALTY="1.05" \
117
+ SYSTEM_PROMPT="" \
118
+ # Number of retries when downloading the model file from the Hub.
119
+ DOWNLOAD_MAX_RETRIES="5" \
120
+ # Gradio server bind settings (must be 0.0.0.0 + 7860 for HF Spaces).
121
+ GRADIO_SERVER_NAME="0.0.0.0" \
122
+ GRADIO_SERVER_PORT="7860" \
123
+ PYTHONUNBUFFERED="1"
124
+
125
+ # Cache/data directories must be writable by the non-root user.
126
+ RUN mkdir -p /data/models /data/hf_home && \
127
+ chown -R appuser:appuser /data /app
128
+
129
+ USER appuser
130
+
131
+ EXPOSE 7860
132
+
133
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,184 @@
1
  ---
2
- title: DOC
3
- emoji: 👁
4
- colorFrom: green
5
- colorTo: yellow
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Qwythos 9B GGUF Chat
3
+ emoji: 🧠
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: apache-2.0
10
  ---
11
 
12
+ # Qwythos-9B GGUF Chat (Docker Space)
13
+
14
+ A self-contained Hugging Face **Docker Space** that downloads a GGUF quant of
15
+ [`empero-ai/Qwythos-9B-Claude-Mythos-5-1M-GGUF`](https://huggingface.co/empero-ai/Qwythos-9B-Claude-Mythos-5-1M-GGUF)
16
+ and serves it through a streaming Gradio chat UI, powered by
17
+ [`llama-cpp-python`](https://github.com/abetlen/llama-cpp-python).
18
+
19
+ > ⚠️ **About this model.** This is a third-party community fine-tune (based
20
+ > on Qwen3.5-9B), not an Anthropic model. Its model card describes it as
21
+ > "uncensored" with no built-in safety layer. If you deploy this Space
22
+ > publicly, add your own moderation/review layer appropriate to your
23
+ > audience — the model card recommends the same.
24
+
25
+ ---
26
+
27
+ ## What's in this repo
28
+
29
+ | File | Purpose |
30
+ |-------------------|-----------------------------------------------------------------------|
31
+ | `Dockerfile` | Multi-stage build: compiles `llama-cpp-python` in a builder stage, then ships a slim runtime image with no compilers. |
32
+ | `requirements.txt`| Pure-Python runtime deps (`gradio`, `huggingface_hub`). |
33
+ | `app.py` | Downloads/caches the GGUF, loads it with `llama-cpp-python`, and serves a streaming Gradio chat UI. |
34
+ | `README.md` | This file (also the Space's metadata card, via the YAML frontmatter above). |
35
+
36
+ This repo is ready to push directly to a new **Docker** Space with no
37
+ further edits.
38
+
39
+ ---
40
+
41
+ ## Deploying to Hugging Face Spaces
42
+
43
+ 1. Create a new Space at <https://huggingface.co/new-space>.
44
+ 2. Choose **Docker** as the Space SDK (not "Gradio" or "Streamlit" — this
45
+ project builds and runs its own Dockerfile).
46
+ 3. Pick the **CPU basic (free)** hardware tier — this project is built to
47
+ run entirely on CPU.
48
+ 4. Push these four files to the Space repo:
49
+
50
+ ```bash
51
+ git clone https://huggingface.co/spaces/<your-username>/<your-space-name>
52
+ cd <your-space-name>
53
+ cp /path/to/Dockerfile /path/to/requirements.txt /path/to/app.py /path/to/README.md .
54
+ git add .
55
+ git commit -m "Deploy Qwythos-9B GGUF chat Space"
56
+ git push
57
+ ```
58
+
59
+ 5. The Space will build the Docker image (this takes several minutes the
60
+ first time — `llama-cpp-python` is compiled from source) and then start
61
+ the container. On first launch, `app.py` downloads the selected GGUF
62
+ file from the Hub in the background; watch the Space's **Logs** tab for
63
+ download/load progress.
64
+ 6. Once the model finishes loading, the chat UI becomes responsive.
65
+
66
+ No secrets or tokens are required for the default (public) repo. If you
67
+ point this at a **gated/private** GGUF repo, add an `HF_TOKEN` secret in the
68
+ Space's **Settings → Variables and secrets**; `huggingface_hub` picks it up
69
+ automatically.
70
+
71
+ ---
72
+
73
+ ## Configuration (environment variables)
74
+
75
+ All of these are set with sensible defaults in the `Dockerfile` and can be
76
+ overridden per-Space under **Settings → Variables and secrets** without
77
+ touching any code:
78
+
79
+ | Variable | Default | Description |
80
+ |-----------------------|-------------------------------------------------------|--------------|
81
+ | `GGUF_REPO_ID` | `empero-ai/Qwythos-9B-Claude-Mythos-5-1M-GGUF` | Hub repo to pull the GGUF from. |
82
+ | `GGUF_FILENAME` | *(empty = auto-select)* | Force an exact filename instead of auto-selecting by quant. |
83
+ | `PREFERRED_QUANT` | `Q4_K_M` | Preferred quantization. Falls back automatically (`Q4_K_S` → `Q5_K_M` → ... → smallest available `.gguf`) if not present. `mmproj` (vision) and `-MTP-` (speculative-decoding draft head) files are skipped by the auto-selector in favor of a plain text-chat quant. |
84
+ | `MODEL_CACHE_DIR` | `/data/models` | Local cache directory for downloaded model weights. |
85
+ | `HF_HOME` | `/data/hf_home` | Cache directory for Hub metadata. |
86
+ | `N_CTX` | `8192` | Context window (tokens) allocated at load time. The model supports up to **1,048,576** tokens via baked-in YaRN scaling, but a free 16GB-RAM CPU Space cannot allocate a multi-hundred-thousand-token KV-cache — raise this only if you've upgraded hardware (see "Long context" below). |
87
+ | `N_THREADS` | `0` (= auto, all cores) | CPU threads for inference. |
88
+ | `N_BATCH` | `256` | Prompt processing batch size. |
89
+ | `MAX_NEW_TOKENS` | `1024` | Max tokens generated per reply. |
90
+ | `TEMPERATURE` | `0.6` | Sampling temperature (the model card recommends 0.6 for its thinking mode; avoid ≤0.3, which the card notes can cause repetition loops). |
91
+ | `TOP_P` | `0.95` | Nucleus sampling. |
92
+ | `TOP_K` | `20` | Top-k sampling. |
93
+ | `REPEAT_PENALTY` | `1.05` | Repetition penalty. |
94
+ | `SYSTEM_PROMPT` | *(empty)* | Optional system prompt prepended to every conversation. |
95
+ | `DOWNLOAD_MAX_RETRIES` | `5` | Retry attempts (exponential backoff) for the model download. |
96
+
97
+ ---
98
+
99
+ ## Model caching & the free tier's storage caveat
100
+
101
+ `app.py` downloads the model once into `MODEL_CACHE_DIR` and reuses the
102
+ cached file for every subsequent chat request — it will **not** re-download
103
+ on every message, and it survives the container going to sleep/waking back
104
+ up from inactivity.
105
+
106
+ However, **the free Spaces tier has no *persistent* storage**: the
107
+ container's disk (including `/data`) is rebuilt from scratch whenever the
108
+ Space is fully **restarted or rebuilt** (e.g. after a `git push`, a factory
109
+ reboot, or an infrastructure migration). In that case, the model will be
110
+ re-downloaded once on the next startup — this is a platform limitation, not
111
+ a bug in this app. If you need the cache to survive restarts, enable
112
+ **Persistent Storage** for the Space (a paid add-on) and point
113
+ `MODEL_CACHE_DIR`/`HF_HOME` at the mounted persistent volume (typically
114
+ `/data`, which is already the default here).
115
+
116
+ ---
117
+
118
+ ## Long context ("1M context") notes
119
+
120
+ The GGUF files in this repo ship with YaRN rope-scaling baked in for up to a
121
+ 1,048,576-token context window. That is a *ceiling*, not something you get
122
+ for free on CPU:
123
+
124
+ - Free **CPU basic** Spaces (16GB RAM) can realistically handle a `Q4_K_M`
125
+ 9B model with a context window in the **low thousands to ~16-32k tokens**,
126
+ depending on available RAM after the model weights are loaded.
127
+ - Attempting to set `N_CTX` far beyond what your Space's RAM allows will
128
+ cause the model load to fail or the container to be OOM-killed.
129
+ - If you need genuinely long context (hundreds of thousands of tokens),
130
+ you'll need a GPU Space or a machine with substantially more RAM — the
131
+ model card itself notes the full 1M window typically needs multi-GPU or
132
+ aggressive KV-cache offload even outside of CPU constraints.
133
+
134
+ `N_CTX` is fully configurable via the environment variable above so you can
135
+ tune it to whatever hardware tier you're running on.
136
+
137
+ ---
138
+
139
+ ## Local development (outside Docker)
140
+
141
+ ```bash
142
+ # Build llama-cpp-python with a build appropriate for your machine:
143
+ CMAKE_ARGS="-DGGML_NATIVE=ON" pip install llama-cpp-python
144
+ pip install -r requirements.txt
145
+
146
+ python app.py
147
+ # then open http://localhost:7860
148
+ ```
149
+
150
+ ## Building/running the Docker image locally
151
+
152
+ ```bash
153
+ docker build -t qwythos-space .
154
+ docker run -it -p 7860:7860 qwythos-space
155
+ # then open http://localhost:7860
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Technical notes
161
+
162
+ - **`llama-cpp-python` build**: built from source in a dedicated builder
163
+ stage with `CMAKE_ARGS="-DGGML_NATIVE=OFF -DGGML_AVX2=ON -DGGML_FMA=ON
164
+ -DGGML_F16C=ON"`. `GGML_NATIVE` is deliberately disabled because the
165
+ machine that *builds* the Docker image is not guaranteed to be the same
166
+ CPU that *runs* it; auto-detected "native" builds can otherwise crash
167
+ with `SIGILL` on the Space's actual runner. AVX2/FMA/F16C are supported
168
+ by essentially all modern cloud x86_64 CPUs and give good performance
169
+ without that risk.
170
+ - **Chat template**: the `Llama` object is created without a hardcoded
171
+ `chat_format`, so `llama-cpp-python` auto-detects and applies the Jinja
172
+ chat template embedded in the GGUF's own metadata — the current,
173
+ non-deprecated approach (no reliance on a manually-specified/legacy
174
+ template name).
175
+ - **Streaming**: implemented via `llm.create_chat_completion(..., stream=True)`,
176
+ yielding incrementally-growing text to Gradio's `ChatInterface` for
177
+ token-by-token display.
178
+ - **GPU layers**: `n_gpu_layers=0` — this Space is CPU-only by design, matching
179
+ the free Spaces hardware tier.
180
+ - **File selection**: uses `huggingface_hub.HfApi().model_info(..., files_metadata=True)`
181
+ to inspect all files with sizes, filters out `mmproj` (vision projector)
182
+ and `-MTP-` (speculative decoding draft-head) variants by default, then
183
+ picks the smallest file matching `PREFERRED_QUANT`, falling back through
184
+ a quant-quality-ordered list if needed.
app.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Docker Space: streaming chat UI for a GGUF model served with
3
+ llama-cpp-python.
4
+
5
+ Model : empero-ai/Qwythos-9B-Claude-Mythos-5-1M-GGUF (configurable via env)
6
+ Quant : Q4_K_M by default, with automatic fallback to another available
7
+ quantization if Q4_K_M isn't present in the repo.
8
+ UI : Gradio ChatInterface, token-by-token streaming.
9
+
10
+ Everything here is driven by environment variables (see Dockerfile / README)
11
+ so the Space can be reconfigured entirely from the "Variables and secrets"
12
+ tab without editing code.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import os
19
+ import threading
20
+ import time
21
+ from typing import Iterator
22
+
23
+ import gradio as gr
24
+ from huggingface_hub import HfApi, hf_hub_download
25
+ from huggingface_hub.utils import HfHubHTTPError
26
+
27
+ logging.basicConfig(
28
+ level=logging.INFO,
29
+ format="%(asctime)s [%(levelname)s] %(message)s",
30
+ )
31
+ log = logging.getLogger("qwythos-space")
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Configuration (all overridable via environment variables)
35
+ # ---------------------------------------------------------------------------
36
+
37
+ GGUF_REPO_ID = os.environ.get("GGUF_REPO_ID", "empero-ai/Qwythos-9B-Claude-Mythos-5-1M-GGUF")
38
+ GGUF_FILENAME = os.environ.get("GGUF_FILENAME", "").strip() # force exact file if set
39
+ PREFERRED_QUANT = os.environ.get("PREFERRED_QUANT", "Q4_K_M").strip()
40
+
41
+ MODEL_CACHE_DIR = os.environ.get("MODEL_CACHE_DIR", "/data/models")
42
+ os.environ.setdefault("HF_HOME", os.environ.get("HF_HOME", "/data/hf_home"))
43
+
44
+ N_CTX = int(os.environ.get("N_CTX", "8192"))
45
+ N_THREADS_ENV = int(os.environ.get("N_THREADS", "0"))
46
+ N_BATCH = int(os.environ.get("N_BATCH", "256"))
47
+ MAX_NEW_TOKENS = int(os.environ.get("MAX_NEW_TOKENS", "1024"))
48
+ TEMPERATURE = float(os.environ.get("TEMPERATURE", "0.6"))
49
+ TOP_P = float(os.environ.get("TOP_P", "0.95"))
50
+ TOP_K = int(os.environ.get("TOP_K", "20"))
51
+ REPEAT_PENALTY = float(os.environ.get("REPEAT_PENALTY", "1.05"))
52
+ SYSTEM_PROMPT = os.environ.get("SYSTEM_PROMPT", "").strip()
53
+
54
+ DOWNLOAD_MAX_RETRIES = int(os.environ.get("DOWNLOAD_MAX_RETRIES", "5"))
55
+
56
+ # Fallback quantization preference order if PREFERRED_QUANT isn't available.
57
+ QUANT_FALLBACK_ORDER = [
58
+ "Q4_K_M", "Q4_K_S", "Q4_0",
59
+ "Q5_K_M", "Q5_K_S",
60
+ "Q6_K", "Q8_0",
61
+ "Q3_K_M", "Q3_K_S",
62
+ "IQ4_XS",
63
+ ]
64
+
65
+ os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Global (lazily-initialised, thread-guarded) model state
69
+ # ---------------------------------------------------------------------------
70
+
71
+ _llm = None
72
+ _llm_lock = threading.Lock()
73
+ _init_status = {"ready": False, "error": None, "model_path": None, "filename": None}
74
+
75
+
76
+ def _is_excluded(filename: str) -> bool:
77
+ """Filter out files we never want to auto-select as the chat model."""
78
+ lower = filename.lower()
79
+ if not lower.endswith(".gguf"):
80
+ return True
81
+ if "mmproj" in lower: # vision projector, not a text model
82
+ return True
83
+ if lower.endswith(".sha256") or lower.endswith(".json"):
84
+ return True
85
+ return False
86
+
87
+
88
+ def _select_gguf_file(repo_id: str, preferred_quant: str) -> str:
89
+ """
90
+ Inspect the repo's file list (with sizes) and pick the best GGUF file:
91
+
92
+ 1. If an exact match for the preferred quant exists among "plain"
93
+ (non multi-token-prediction / non mmproj) files, use it.
94
+ 2. Otherwise, fall back through QUANT_FALLBACK_ORDER.
95
+ 3. Otherwise, fall back to the smallest remaining .gguf file (best
96
+ chance of fitting/running on modest CPU hardware).
97
+
98
+ "MTP" (multi-token-prediction draft-head) variants are deprioritized:
99
+ they require extra `--spec-type draft-mtp` speculative-decoding flags
100
+ that plain llama-cpp-python chat completion does not use, so a plain
101
+ quant is the safer default for a generic chat UI.
102
+ """
103
+ api = HfApi()
104
+ info = api.model_info(repo_id, files_metadata=True)
105
+ siblings = [s for s in info.siblings if not _is_excluded(s.rfilename)]
106
+ if not siblings:
107
+ raise RuntimeError(f"No usable .gguf files found in repo '{repo_id}'.")
108
+
109
+ def is_mtp(name: str) -> bool:
110
+ low = name.lower()
111
+ return "-mtp-" in low or low.startswith("mtp-") or "_mtp_" in low
112
+
113
+ plain = [s for s in siblings if not is_mtp(s.rfilename)]
114
+ pool = plain if plain else siblings # only use MTP files if nothing else exists
115
+
116
+ # 1. Exact preferred-quant match among the pool, smallest file wins ties.
117
+ exact = [s for s in pool if preferred_quant.lower() in s.rfilename.lower()]
118
+ if exact:
119
+ best = min(exact, key=lambda s: (s.size or float("inf")))
120
+ return best.rfilename
121
+
122
+ # 2. Fallback quant order.
123
+ for quant in QUANT_FALLBACK_ORDER:
124
+ matches = [s for s in pool if quant.lower() in s.rfilename.lower()]
125
+ if matches:
126
+ best = min(matches, key=lambda s: (s.size or float("inf")))
127
+ log.warning(
128
+ "Preferred quant '%s' not found in %s; falling back to '%s' (%s).",
129
+ preferred_quant, repo_id, quant, best.rfilename,
130
+ )
131
+ return best.rfilename
132
+
133
+ # 3. Last resort: smallest .gguf available at all.
134
+ best = min(pool, key=lambda s: (s.size or float("inf")))
135
+ log.warning(
136
+ "No known quant matched preferences in %s; falling back to smallest "
137
+ "available file: %s", repo_id, best.rfilename,
138
+ )
139
+ return best.rfilename
140
+
141
+
142
+ def _download_with_retries(repo_id: str, filename: str, cache_dir: str, max_retries: int) -> str:
143
+ """Download (or reuse the local cache for) a file from the Hub, with
144
+ exponential-backoff retries so transient network issues don't crash the
145
+ Space on startup."""
146
+ last_err: Exception | None = None
147
+ for attempt in range(1, max_retries + 1):
148
+ try:
149
+ log.info("Downloading %s (attempt %d/%d)...", filename, attempt, max_retries)
150
+ path = hf_hub_download(
151
+ repo_id=repo_id,
152
+ filename=filename,
153
+ cache_dir=cache_dir,
154
+ # local_files_only=False -> if already cached, this returns
155
+ # instantly without re-downloading (cache is content-hashed).
156
+ )
157
+ log.info("Model file ready at: %s", path)
158
+ return path
159
+ except (HfHubHTTPError, OSError, ValueError) as exc:
160
+ last_err = exc
161
+ wait = min(2 ** attempt, 30)
162
+ log.warning("Download attempt %d failed (%s). Retrying in %ds...", attempt, exc, wait)
163
+ time.sleep(wait)
164
+ raise RuntimeError(
165
+ f"Failed to download '{filename}' from '{repo_id}' after {max_retries} attempts: {last_err}"
166
+ ) from last_err
167
+
168
+
169
+ def _load_llm():
170
+ """Resolve, download and load the GGUF model. Populates module-level
171
+ state; raises on unrecoverable failure (caught by the caller)."""
172
+ from llama_cpp import Llama # imported here so a load failure doesn't
173
+
174
+ filename = GGUF_FILENAME or _select_gguf_file(GGUF_REPO_ID, PREFERRED_QUANT)
175
+ log.info("Selected GGUF file: %s", filename)
176
+
177
+ model_path = _download_with_retries(
178
+ repo_id=GGUF_REPO_ID,
179
+ filename=filename,
180
+ cache_dir=MODEL_CACHE_DIR,
181
+ max_retries=DOWNLOAD_MAX_RETRIES,
182
+ )
183
+
184
+ n_threads = N_THREADS_ENV if N_THREADS_ENV > 0 else max(1, os.cpu_count() or 4)
185
+ log.info(
186
+ "Loading model into llama.cpp (n_ctx=%d, n_threads=%d, n_batch=%d)...",
187
+ N_CTX, n_threads, N_BATCH,
188
+ )
189
+
190
+ llm = Llama(
191
+ model_path=model_path,
192
+ n_ctx=N_CTX,
193
+ n_threads=n_threads,
194
+ n_batch=N_BATCH,
195
+ n_gpu_layers=0, # CPU-only Space: keep everything on CPU.
196
+ # chat_format left as None (default): llama-cpp-python auto-detects
197
+ # and applies the chat template embedded in the GGUF's metadata
198
+ # (the model ships its own Jinja chat template), which is the
199
+ # current, non-deprecated way to get correct prompting without
200
+ # hardcoding a template name.
201
+ verbose=False,
202
+ )
203
+ _init_status.update(ready=True, error=None, model_path=model_path, filename=filename)
204
+ return llm
205
+
206
+
207
+ def get_llm():
208
+ """Thread-safe lazy singleton accessor for the loaded model."""
209
+ global _llm
210
+ if _llm is not None:
211
+ return _llm
212
+ with _llm_lock:
213
+ if _llm is None:
214
+ try:
215
+ _llm = _load_llm()
216
+ except Exception as exc: # noqa: BLE001 - surface any failure to the UI
217
+ log.exception("Model initialization failed.")
218
+ _init_status.update(ready=False, error=str(exc))
219
+ raise
220
+ return _llm
221
+
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # Chat inference
225
+ # ---------------------------------------------------------------------------
226
+
227
+ def _build_messages(message: str, history: list[dict]) -> list[dict]:
228
+ messages: list[dict] = []
229
+ if SYSTEM_PROMPT:
230
+ messages.append({"role": "system", "content": SYSTEM_PROMPT})
231
+ # `history` from gr.ChatInterface(type="messages") is already a list of
232
+ # {"role": ..., "content": ...} dicts.
233
+ messages.extend(history)
234
+ messages.append({"role": "user", "content": message})
235
+ return messages
236
+
237
+
238
+ def respond(message: str, history: list[dict]) -> Iterator[str]:
239
+ """Gradio streaming callback: yields the growing response string as new
240
+ tokens arrive from llama.cpp's chat-completion stream."""
241
+ try:
242
+ llm = get_llm()
243
+ except Exception as exc: # noqa: BLE001
244
+ yield (
245
+ "⚠️ The model failed to load, so I can't respond right now.\n\n"
246
+ f"**Error:** {exc}\n\n"
247
+ "Check the Space's logs for details. If this is a download error, "
248
+ "it will often resolve itself on a retry/restart; if it persists, "
249
+ "verify `GGUF_REPO_ID` / `GGUF_FILENAME` are correct and that the "
250
+ "repo/file are publicly accessible."
251
+ )
252
+ return
253
+
254
+ messages = _build_messages(message, history)
255
+
256
+ try:
257
+ stream = llm.create_chat_completion(
258
+ messages=messages,
259
+ max_tokens=MAX_NEW_TOKENS,
260
+ temperature=TEMPERATURE,
261
+ top_p=TOP_P,
262
+ top_k=TOP_K,
263
+ repeat_penalty=REPEAT_PENALTY,
264
+ stream=True,
265
+ )
266
+ except Exception as exc: # noqa: BLE001
267
+ yield f"⚠️ Generation failed: {exc}"
268
+ return
269
+
270
+ partial = ""
271
+ for chunk in stream:
272
+ choice = chunk.get("choices", [{}])[0]
273
+ delta = choice.get("delta", {})
274
+ token = delta.get("content")
275
+ if token:
276
+ partial += token
277
+ yield partial
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # Gradio UI
282
+ # ---------------------------------------------------------------------------
283
+
284
+ def _status_markdown() -> str:
285
+ return (
286
+ f"**Model repo:** `{GGUF_REPO_ID}` \n"
287
+ f"**Requested quant:** `{PREFERRED_QUANT}`" +
288
+ (f" (forced file: `{GGUF_FILENAME}`)" if GGUF_FILENAME else "") + " \n"
289
+ f"**Context window:** {N_CTX:,} tokens \n"
290
+ "*The model downloads on first request and is cached for the life "
291
+ "of this running container.*"
292
+ )
293
+
294
+
295
+ with gr.Blocks(title="Qwythos-9B Chat") as demo:
296
+ gr.Markdown("# 🧠 Qwythos-9B Chat (GGUF / llama.cpp)")
297
+ gr.Markdown(_status_markdown())
298
+ gr.Markdown(
299
+ "> ⚠️ This model's card describes it as an uncensored fine-tune with "
300
+ "no built-in safety layer. If you deploy this publicly, consider "
301
+ "adding your own moderation/review layer."
302
+ )
303
+
304
+ gr.ChatInterface(
305
+ fn=respond,
306
+ type="messages",
307
+ chatbot=gr.Chatbot(type="messages", height=550, show_copy_button=True),
308
+ textbox=gr.Textbox(
309
+ placeholder="Ask something...",
310
+ scale=7,
311
+ ),
312
+ title=None,
313
+ examples=[
314
+ "Give me a short summary of what you can do.",
315
+ "Explain the difference between a stack and a queue.",
316
+ ],
317
+ cache_examples=False,
318
+ )
319
+
320
+ if __name__ == "__main__":
321
+ # Kick off model loading in the background as soon as the app starts,
322
+ # rather than waiting for the first chat message, so the download/load
323
+ # progress is visible in the Space's build/runtime logs immediately.
324
+ threading.Thread(target=lambda: (get_llm() if not _init_status["ready"] else None), daemon=True).start()
325
+
326
+ demo.queue(max_size=32).launch(
327
+ server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
328
+ server_port=int(os.environ.get("GRADIO_SERVER_PORT", "7860")),
329
+ )
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---------------------------------------------------------------------------
2
+ # Python runtime dependencies.
3
+ #
4
+ # NOTE: llama-cpp-python is intentionally NOT listed here. It is built from
5
+ # source in the Dockerfile's builder stage with explicit CMake flags tuned
6
+ # for CPU-only HF Spaces (see Dockerfile comments), then installed from the
7
+ # resulting wheel. Listing an unpinned "llama-cpp-python" here as well would
8
+ # risk pip re-resolving/reinstalling a different (source) build without
9
+ # those flags. If you need to run app.py OUTSIDE Docker (e.g. local dev),
10
+ # install it manually first:
11
+ #
12
+ # CMAKE_ARGS="-DGGML_NATIVE=ON" pip install llama-cpp-python
13
+ #
14
+ # Then `pip install -r requirements.txt` for the rest.
15
+ # ---------------------------------------------------------------------------
16
+
17
+ gradio>=5.0,<6.0
18
+ huggingface_hub>=0.24.0