Refactor: Docker+llama.cpp -> Gradio SDK + ZeroGPU transformers backend
Browse files- .dockerignore +0 -29
- Dockerfile +0 -52
- README.md +18 -10
- entrypoint.sh +0 -43
- llm/omni_client.py +11 -0
- llm/zerogpu_backend.py +163 -0
- requirements.txt +17 -0
.dockerignore
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
# Keep the build context (and the pushed Space repo) small. Multi-GB vendored
|
| 2 |
-
# trees, the local model runtime, and Windows-only launchers are excluded.
|
| 3 |
-
.git
|
| 4 |
-
.venv
|
| 5 |
-
venv
|
| 6 |
-
runtime/
|
| 7 |
-
tools/
|
| 8 |
-
__pycache__/
|
| 9 |
-
**/__pycache__/
|
| 10 |
-
*.pyc
|
| 11 |
-
tmp/
|
| 12 |
-
*.log
|
| 13 |
-
*.err.log
|
| 14 |
-
|
| 15 |
-
# Heavy / local-only data. The maps, processed graph and voices are kept;
|
| 16 |
-
# backups, saved games and the smoke screenshot are not needed in the image.
|
| 17 |
-
data/backups/
|
| 18 |
-
data/games/
|
| 19 |
-
data/archives/
|
| 20 |
-
data/raw/archives/
|
| 21 |
-
data/*.log
|
| 22 |
-
data/*.png
|
| 23 |
-
|
| 24 |
-
# Windows launchers — not used on Linux.
|
| 25 |
-
run_game.ps1
|
| 26 |
-
run_game.cmd
|
| 27 |
-
|
| 28 |
-
# Build artifact placeholders
|
| 29 |
-
C:*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
DELETED
|
@@ -1,52 +0,0 @@
|
|
| 1 |
-
# Phantom Grid — Docker Hugging Face Space (Linux, CPU).
|
| 2 |
-
#
|
| 3 |
-
# The image bakes in a prebuilt llama.cpp OpenAI-compatible server
|
| 4 |
-
# (llama-cpp-python) and the MiniCPM4.1-8B Q4_K_M GGUF (text backend, <32B),
|
| 5 |
-
# so there is NO build-from-source or model download at container startup.
|
| 6 |
-
FROM python:3.12-slim
|
| 7 |
-
|
| 8 |
-
ENV PYTHONUNBUFFERED=1 \
|
| 9 |
-
PIP_NO_CACHE_DIR=1 \
|
| 10 |
-
HF_HOME=/app/.hfcache \
|
| 11 |
-
MODEL_DIR=/app/models \
|
| 12 |
-
MODEL_PATH=/app/models/MiniCPM4.1-8B-Q4_K_M.gguf
|
| 13 |
-
|
| 14 |
-
# Minimal OS deps. libgomp1 is needed by the llama.cpp OpenMP runtime.
|
| 15 |
-
RUN apt-get update \
|
| 16 |
-
&& apt-get install -y --no-install-recommends libgomp1 curl ca-certificates \
|
| 17 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 18 |
-
|
| 19 |
-
WORKDIR /app
|
| 20 |
-
|
| 21 |
-
# --- Python deps -----------------------------------------------------------
|
| 22 |
-
COPY requirements-space.txt ./
|
| 23 |
-
RUN pip install --upgrade pip \
|
| 24 |
-
&& pip install -r requirements-space.txt \
|
| 25 |
-
# Prebuilt CPU wheel for the OpenAI-compatible llama.cpp server (no compile).
|
| 26 |
-
&& pip install "llama-cpp-python[server]" \
|
| 27 |
-
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
|
| 28 |
-
|
| 29 |
-
# --- Bake the model at BUILD time -----------------------------------------
|
| 30 |
-
# Downloaded into the image so the Space boots without a multi-GB pull.
|
| 31 |
-
RUN python -c "from huggingface_hub import hf_hub_download; \
|
| 32 |
-
hf_hub_download(repo_id='openbmb/MiniCPM4.1-8B-GGUF', \
|
| 33 |
-
filename='MiniCPM4.1-8B-Q4_K_M.gguf', local_dir='/app/models')"
|
| 34 |
-
|
| 35 |
-
# --- App code --------------------------------------------------------------
|
| 36 |
-
COPY . .
|
| 37 |
-
|
| 38 |
-
# Writable dirs for HF's runtime user (Spaces run as uid 1000).
|
| 39 |
-
RUN mkdir -p /app/data/games /app/.hfcache \
|
| 40 |
-
&& chmod -R 777 /app/data /app/.hfcache /app/models
|
| 41 |
-
|
| 42 |
-
# Point the app at the in-container llama.cpp server (text-only, no voice).
|
| 43 |
-
ENV PHANTOM_GRID_LLM_PROVIDER=external_llama_cpp_server \
|
| 44 |
-
PHANTOM_GRID_LLAMACPP_BASE_URL=http://127.0.0.1:8080/v1 \
|
| 45 |
-
PHANTOM_GRID_LLM_MODEL=MiniCPM4.1-8B-Q4_K_M.gguf \
|
| 46 |
-
PHANTOM_GRID_WITNESS_CHAT_TTS=0 \
|
| 47 |
-
PHANTOM_GRID_HOST=0.0.0.0 \
|
| 48 |
-
PORT=7860
|
| 49 |
-
|
| 50 |
-
EXPOSE 7860
|
| 51 |
-
|
| 52 |
-
ENTRYPOINT ["bash", "entrypoint.sh"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -3,16 +3,25 @@ title: Phantom Grid
|
|
| 3 |
emoji: 🕵️
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: gray
|
| 6 |
-
sdk:
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
pinned: false
|
| 9 |
license: mit
|
|
|
|
| 10 |
tags:
|
| 11 |
- thousand-token-wood
|
| 12 |
- delightful
|
| 13 |
- game
|
| 14 |
- agent
|
| 15 |
- minicpm
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
---
|
| 17 |
|
| 18 |
# 🕵️ Phantom Grid
|
|
@@ -33,17 +42,16 @@ in a custom `gr.Server` HTML/JS board interface (no stock Gradio components).
|
|
| 33 |
## 🤖 Model & inference
|
| 34 |
|
| 35 |
- **Model:** [`openbmb/MiniCPM4.1-8B`](https://huggingface.co/openbmb/MiniCPM4.1-8B)
|
| 36 |
-
(text,
|
| 37 |
-
- **Inference:**
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
Space boots without any multi-GB download or build-from-source.
|
| 41 |
|
| 42 |
## 🖥️ Hardware
|
| 43 |
|
| 44 |
-
Runs on **
|
| 45 |
-
|
| 46 |
-
|
| 47 |
|
| 48 |
## 🎥 Demo video
|
| 49 |
|
|
|
|
| 3 |
emoji: 🕵️
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: gray
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.17.3
|
| 8 |
+
app_file: app.py
|
| 9 |
+
python_version: "3.10"
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
+
hf_oauth: false
|
| 13 |
tags:
|
| 14 |
- thousand-token-wood
|
| 15 |
- delightful
|
| 16 |
- game
|
| 17 |
- agent
|
| 18 |
- minicpm
|
| 19 |
+
env:
|
| 20 |
+
- PHANTOM_GRID_LLM_PROVIDER=zerogpu_transformers
|
| 21 |
+
- PHANTOM_GRID_ZEROGPU_MODEL_ID=openbmb/MiniCPM4.1-8B
|
| 22 |
+
- PHANTOM_GRID_ZEROGPU_DURATION=90
|
| 23 |
+
- PHANTOM_GRID_HOST=0.0.0.0
|
| 24 |
+
- PHANTOM_GRID_WITNESS_CHAT_TTS=0
|
| 25 |
---
|
| 26 |
|
| 27 |
# 🕵️ Phantom Grid
|
|
|
|
| 42 |
## 🤖 Model & inference
|
| 43 |
|
| 44 |
- **Model:** [`openbmb/MiniCPM4.1-8B`](https://huggingface.co/openbmb/MiniCPM4.1-8B)
|
| 45 |
+
(text, bf16 transformers) — ~8B params, well under the 32B cap.
|
| 46 |
+
- **Inference:** in-process Hugging Face `transformers`. The model lives in CPU
|
| 47 |
+
RAM and is moved to cuda inside a `@spaces.GPU`-decorated function, so the
|
| 48 |
+
H200 is only held while the model is actually generating.
|
|
|
|
| 49 |
|
| 50 |
## 🖥️ Hardware
|
| 51 |
|
| 52 |
+
Runs on **ZeroGPU** (H200, free 40 min/day for org members). Each generation is
|
| 53 |
+
capped at `PHANTOM_GRID_ZEROGPU_DURATION` seconds (default 90). No voice path
|
| 54 |
+
on this Space (`PHANTOM_GRID_WITNESS_CHAT_TTS=0`).
|
| 55 |
|
| 56 |
## 🎥 Demo video
|
| 57 |
|
entrypoint.sh
DELETED
|
@@ -1,43 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env bash
|
| 2 |
-
# Linux entrypoint for the Phantom Grid Docker Space.
|
| 3 |
-
# 1. Start the baked-in llama.cpp OpenAI-compatible server on :8080.
|
| 4 |
-
# 2. Wait until it reports healthy (/v1/models).
|
| 5 |
-
# 3. Launch the gr.Server app, bound to 0.0.0.0:$PORT for the Space.
|
| 6 |
-
set -euo pipefail
|
| 7 |
-
|
| 8 |
-
MODEL_PATH="${MODEL_PATH:-/app/models/MiniCPM4.1-8B-Q4_K_M.gguf}"
|
| 9 |
-
LLAMA_PORT="${LLAMA_PORT:-8080}"
|
| 10 |
-
CTX="${PHANTOM_GRID_LLAMACPP_CONTEXT_LENGTH:-8192}"
|
| 11 |
-
APP_PORT="${PORT:-7860}"
|
| 12 |
-
|
| 13 |
-
echo "[entrypoint] Starting llama.cpp server on :${LLAMA_PORT} with ${MODEL_PATH}"
|
| 14 |
-
# llama_cpp.server's pydantic Settings reads HOST/PORT from env and they
|
| 15 |
-
# override the CLI flags — so on HF Spaces (PORT=7860) the server would bind
|
| 16 |
-
# 7860 and collide with Gradio. Scope HOST/PORT to just this subprocess.
|
| 17 |
-
HOST=127.0.0.1 PORT="${LLAMA_PORT}" python -m llama_cpp.server \
|
| 18 |
-
--model "${MODEL_PATH}" \
|
| 19 |
-
--host 127.0.0.1 \
|
| 20 |
-
--port "${LLAMA_PORT}" \
|
| 21 |
-
--n_ctx "${CTX}" \
|
| 22 |
-
--n_gpu_layers "${LLAMA_N_GPU_LAYERS:-0}" \
|
| 23 |
-
&
|
| 24 |
-
LLAMA_PID=$!
|
| 25 |
-
|
| 26 |
-
cleanup() { kill "${LLAMA_PID}" 2>/dev/null || true; }
|
| 27 |
-
trap cleanup EXIT INT TERM
|
| 28 |
-
|
| 29 |
-
echo "[entrypoint] Waiting for llama.cpp /v1/models to become ready..."
|
| 30 |
-
for i in $(seq 1 180); do
|
| 31 |
-
if curl -sf "http://127.0.0.1:${LLAMA_PORT}/v1/models" >/dev/null 2>&1; then
|
| 32 |
-
echo "[entrypoint] llama.cpp server is ready."
|
| 33 |
-
break
|
| 34 |
-
fi
|
| 35 |
-
if ! kill -0 "${LLAMA_PID}" 2>/dev/null; then
|
| 36 |
-
echo "[entrypoint] llama.cpp server exited unexpectedly." >&2
|
| 37 |
-
exit 1
|
| 38 |
-
fi
|
| 39 |
-
sleep 2
|
| 40 |
-
done
|
| 41 |
-
|
| 42 |
-
echo "[entrypoint] Launching Phantom Grid app on 0.0.0.0:${APP_PORT}"
|
| 43 |
-
exec python app.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
llm/omni_client.py
CHANGED
|
@@ -35,6 +35,9 @@ class OmniClient:
|
|
| 35 |
return cls(load_settings())
|
| 36 |
|
| 37 |
def health(self, timeout: float = 0.75) -> dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
| 38 |
if self.settings.llm_provider in {"llama_cpp_server", "external_llama_cpp_server"}:
|
| 39 |
base_url = self.settings.llamacpp_base_url.rstrip("/")
|
| 40 |
try:
|
|
@@ -358,6 +361,14 @@ class OmniClient:
|
|
| 358 |
{"role": "system", "content": system_prompt},
|
| 359 |
{"role": "user", "content": user_prompt},
|
| 360 |
])]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
if self.settings.llm_provider in {"llama_cpp_server", "external_llama_cpp_server"}:
|
| 362 |
text_url = f"{self.settings.llamacpp_base_url.rstrip('/')}/chat/completions"
|
| 363 |
model = self.settings.llm_model or (self.settings.llamacpp_model_path.name if self.settings.llamacpp_model_path else "local")
|
|
|
|
| 35 |
return cls(load_settings())
|
| 36 |
|
| 37 |
def health(self, timeout: float = 0.75) -> dict[str, Any]:
|
| 38 |
+
if self.settings.llm_provider == "zerogpu_transformers":
|
| 39 |
+
from llm import zerogpu_backend
|
| 40 |
+
return zerogpu_backend.health()
|
| 41 |
if self.settings.llm_provider in {"llama_cpp_server", "external_llama_cpp_server"}:
|
| 42 |
base_url = self.settings.llamacpp_base_url.rstrip("/")
|
| 43 |
try:
|
|
|
|
| 361 |
{"role": "system", "content": system_prompt},
|
| 362 |
{"role": "user", "content": user_prompt},
|
| 363 |
])]
|
| 364 |
+
if self.settings.llm_provider == "zerogpu_transformers":
|
| 365 |
+
from llm import zerogpu_backend
|
| 366 |
+
return zerogpu_backend.chat_completion(
|
| 367 |
+
content,
|
| 368 |
+
temperature=temperature,
|
| 369 |
+
max_tokens=max_tokens,
|
| 370 |
+
json_mode=json_mode,
|
| 371 |
+
)
|
| 372 |
if self.settings.llm_provider in {"llama_cpp_server", "external_llama_cpp_server"}:
|
| 373 |
text_url = f"{self.settings.llamacpp_base_url.rstrip('/')}/chat/completions"
|
| 374 |
model = self.settings.llm_model or (self.settings.llamacpp_model_path.name if self.settings.llamacpp_model_path else "local")
|
llm/zerogpu_backend.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""In-process transformers backend for HF Spaces ZeroGPU.
|
| 2 |
+
|
| 3 |
+
Loaded only when PHANTOM_GRID_LLM_PROVIDER=zerogpu_transformers. Model weights
|
| 4 |
+
live in CPU RAM and are moved to cuda inside the @spaces.GPU function, which is
|
| 5 |
+
how ZeroGPU's per-call GPU acquisition expects PyTorch models to behave.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import os
|
| 11 |
+
import re
|
| 12 |
+
import threading
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
_MODEL_ID = os.getenv("PHANTOM_GRID_ZEROGPU_MODEL_ID", "openbmb/MiniCPM4.1-8B")
|
| 16 |
+
_DEFAULT_DURATION = int(os.getenv("PHANTOM_GRID_ZEROGPU_DURATION", "90"))
|
| 17 |
+
|
| 18 |
+
_lock = threading.Lock()
|
| 19 |
+
_model = None
|
| 20 |
+
_tokenizer = None
|
| 21 |
+
_loaded_to_cuda = False
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _ensure_loaded() -> None:
|
| 25 |
+
"""Load the model + tokenizer onto CPU. Called once at module init."""
|
| 26 |
+
global _model, _tokenizer
|
| 27 |
+
if _model is not None and _tokenizer is not None:
|
| 28 |
+
return
|
| 29 |
+
import torch
|
| 30 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 31 |
+
|
| 32 |
+
with _lock:
|
| 33 |
+
if _tokenizer is None:
|
| 34 |
+
_tokenizer = AutoTokenizer.from_pretrained(_MODEL_ID, trust_remote_code=True)
|
| 35 |
+
if _model is None:
|
| 36 |
+
_model = AutoModelForCausalLM.from_pretrained(
|
| 37 |
+
_MODEL_ID,
|
| 38 |
+
torch_dtype=torch.bfloat16,
|
| 39 |
+
trust_remote_code=True,
|
| 40 |
+
low_cpu_mem_usage=True,
|
| 41 |
+
)
|
| 42 |
+
_model.eval()
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
import spaces # type: ignore
|
| 47 |
+
|
| 48 |
+
_spaces_gpu = spaces.GPU
|
| 49 |
+
except ImportError: # pragma: no cover — local dev without `spaces` installed
|
| 50 |
+
def _spaces_gpu(*_args, **_kwargs):
|
| 51 |
+
def _wrap(fn):
|
| 52 |
+
return fn
|
| 53 |
+
return _wrap
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@_spaces_gpu(duration=_DEFAULT_DURATION)
|
| 57 |
+
def _generate_on_gpu(
|
| 58 |
+
messages: list[dict[str, Any]],
|
| 59 |
+
max_new_tokens: int,
|
| 60 |
+
temperature: float,
|
| 61 |
+
json_mode: bool,
|
| 62 |
+
) -> str:
|
| 63 |
+
"""Runs on the H200 only when ZeroGPU has granted the slot."""
|
| 64 |
+
import torch
|
| 65 |
+
|
| 66 |
+
global _loaded_to_cuda
|
| 67 |
+
_ensure_loaded()
|
| 68 |
+
assert _model is not None and _tokenizer is not None
|
| 69 |
+
|
| 70 |
+
if not _loaded_to_cuda:
|
| 71 |
+
_model.to("cuda")
|
| 72 |
+
_loaded_to_cuda = True
|
| 73 |
+
|
| 74 |
+
prompt = _tokenizer.apply_chat_template(
|
| 75 |
+
messages,
|
| 76 |
+
tokenize=False,
|
| 77 |
+
add_generation_prompt=True,
|
| 78 |
+
)
|
| 79 |
+
inputs = _tokenizer(prompt, return_tensors="pt").to("cuda")
|
| 80 |
+
|
| 81 |
+
do_sample = temperature > 0
|
| 82 |
+
gen_kwargs = {
|
| 83 |
+
"max_new_tokens": max_new_tokens,
|
| 84 |
+
"do_sample": do_sample,
|
| 85 |
+
"pad_token_id": _tokenizer.eos_token_id,
|
| 86 |
+
}
|
| 87 |
+
if do_sample:
|
| 88 |
+
gen_kwargs["temperature"] = temperature
|
| 89 |
+
gen_kwargs["top_p"] = 0.9
|
| 90 |
+
|
| 91 |
+
with torch.inference_mode():
|
| 92 |
+
outputs = _model.generate(**inputs, **gen_kwargs)
|
| 93 |
+
|
| 94 |
+
new_tokens = outputs[0][inputs["input_ids"].shape[1]:]
|
| 95 |
+
text = _tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
|
| 96 |
+
|
| 97 |
+
if json_mode:
|
| 98 |
+
text = _extract_json(text)
|
| 99 |
+
return text
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _extract_json(text: str) -> str:
|
| 103 |
+
"""Strip code fences and isolate the first JSON object."""
|
| 104 |
+
cleaned = text.strip()
|
| 105 |
+
if cleaned.startswith("```"):
|
| 106 |
+
cleaned = cleaned.strip("`")
|
| 107 |
+
if cleaned.lower().startswith("json"):
|
| 108 |
+
cleaned = cleaned[4:].strip()
|
| 109 |
+
start = cleaned.find("{")
|
| 110 |
+
end = cleaned.rfind("}")
|
| 111 |
+
if start >= 0 and end > start:
|
| 112 |
+
return cleaned[start : end + 1]
|
| 113 |
+
return text
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
_NO_THINK_HINTS = ("minicpm4", "minicpm-4", "minicpm_4", "qwen3", "qwen-3")
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _maybe_inject_no_think(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 120 |
+
"""MiniCPM4 / Qwen3 emit hidden <think> blocks that eat the token budget on
|
| 121 |
+
small max_new_tokens. Append '/no_think' to the system turn so the model
|
| 122 |
+
skips it. Mirrors llm/omni_client.py:_ensure_no_think."""
|
| 123 |
+
if not any(hint in _MODEL_ID.lower() for hint in _NO_THINK_HINTS):
|
| 124 |
+
return messages
|
| 125 |
+
directive = "/no_think"
|
| 126 |
+
for item in messages:
|
| 127 |
+
if item.get("role") == "system" and isinstance(item.get("content"), str):
|
| 128 |
+
if directive not in item["content"]:
|
| 129 |
+
item["content"] = f"{item['content'].rstrip()} {directive}".strip()
|
| 130 |
+
return messages
|
| 131 |
+
return [{"role": "system", "content": directive}, *messages]
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def chat_completion(
|
| 135 |
+
messages: list[dict[str, Any]],
|
| 136 |
+
*,
|
| 137 |
+
temperature: float = 0.4,
|
| 138 |
+
max_tokens: int = 512,
|
| 139 |
+
json_mode: bool = False,
|
| 140 |
+
) -> str:
|
| 141 |
+
"""Synchronous, drop-in replacement for an OpenAI chat-completions POST."""
|
| 142 |
+
messages = _maybe_inject_no_think([dict(m) for m in messages])
|
| 143 |
+
return _generate_on_gpu(messages, max_tokens, temperature, json_mode)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def health() -> dict[str, Any]:
|
| 147 |
+
"""Cheap probe — does NOT touch the GPU. Reflects whether the module imported."""
|
| 148 |
+
return {
|
| 149 |
+
"reachable": True,
|
| 150 |
+
"ready": _tokenizer is not None,
|
| 151 |
+
"detail": {"model_id": _MODEL_ID, "loaded_to_cuda": _loaded_to_cuda},
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# Kick off the CPU-side load eagerly so the first user turn doesn't pay for it.
|
| 156 |
+
# Wrapped in try/except so an import failure (e.g. during local dev without the
|
| 157 |
+
# huge model cache) doesn't crash app startup — the health probe will report it.
|
| 158 |
+
try:
|
| 159 |
+
_ensure_loaded()
|
| 160 |
+
except Exception as exc: # pragma: no cover
|
| 161 |
+
_LOAD_ERROR = f"{exc.__class__.__name__}: {exc}"
|
| 162 |
+
else:
|
| 163 |
+
_LOAD_ERROR = None
|
requirements.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Runtime dependencies for the Gradio SDK / ZeroGPU Hugging Face Space.
|
| 2 |
+
# Read by HF Spaces' Gradio runner at build time.
|
| 3 |
+
|
| 4 |
+
gradio>=6.17.3
|
| 5 |
+
spaces>=0.30.0
|
| 6 |
+
huggingface_hub>=1.2
|
| 7 |
+
pydantic>=2.7
|
| 8 |
+
websockets>=14.0
|
| 9 |
+
fastapi
|
| 10 |
+
uvicorn
|
| 11 |
+
|
| 12 |
+
# Text LLM backend: HF transformers + PyTorch, MiniCPM4.1-8B is loaded
|
| 13 |
+
# CPU-side at import and moved to cuda inside the @spaces.GPU function.
|
| 14 |
+
torch>=2.4.0
|
| 15 |
+
transformers>=4.46.0
|
| 16 |
+
accelerate>=1.0.0
|
| 17 |
+
sentencepiece
|