CPU / app.py
OrbitMC's picture
Update app.py
5e23e7e verified
Raw
History Blame Contribute Delete
8.04 kB
import os, io, json, time, tarfile, subprocess, urllib.request
from pathlib import Path
import gradio as gr
import spaces
import httpx
from huggingface_hub import hf_hub_download
# ===== 0. CONFIG (override in Space Settings β†’ Variables and secrets) =====
LLAMA_TAG = os.environ.get("LLAMA_TAG", "") # pin e.g. "b8407"; "" = latest
MODEL_REPO = os.environ.get("MODEL_REPO", "Abiray/MiniCPM5-1B-GGUF")
MODEL_FILE = os.environ.get("MODEL_FILE", "minicpm5-1b-Q6_K.gguf")
CTX_SIZE = int(os.environ.get("CTX_SIZE", "8192"))
PARALLEL = int(os.environ.get("PARALLEL", "2"))
LLAMA_PORT = int(os.environ.get("LLAMA_PORT", "8080")) # internal only
API_KEY = os.environ.get("LLAMA_API_KEY", "") # SET THIS as a Space secret!
SYSTEM_PROMPT = os.environ.get("SYSTEM_PROMPT", "You are a helpful AI assistant.")
# ===== 1. HOLD THE ZEROGPU HARDWARE (~5s of quota, once, at boot) =====
@spaces.GPU(duration=5)
def _hold_zerogpu():
print("[zerogpu] host claimed; GPU slice released, CPU+RAM stay ours.")
return True
_hold_zerogpu()
# ===== 2. READ THE "DYNAMIC" CPU/RAM WE ACTUALLY GOT (cgroup truth) =====
def effective_cpus() -> int:
try:
quota, period = Path("/sys/fs/cgroup/cpu.max").read_text().split()[:2]
if quota != "max":
return max(1, int(quota) // int(period))
except Exception:
pass
try:
return len(os.sched_getaffinity(0))
except Exception:
return os.cpu_count() or 2
def memory_limit_gb():
try:
v = Path("/sys/fs/cgroup/memory.max").read_text().strip()
if v != "max":
return round(int(v) / 1e9, 1)
except Exception:
pass
return None
CORES = effective_cpus()
print(f"[resources] effective cores: {CORES} | RAM limit: {memory_limit_gb() or '?'} GB")
# ===== 3. FETCH PREBUILT llama.cpp (seconds, zero compilation) =====
FALLBACK_TAG = "b8407"
def resolve_tag() -> str:
if LLAMA_TAG:
return LLAMA_TAG
try:
req = urllib.request.Request(
"https://api.github.com/repos/ggml-org/llama.cpp/releases/latest",
headers={"User-Agent": "hf-space"})
with urllib.request.urlopen(req, timeout=10) as r:
return json.load(r)["tag_name"]
except Exception as e:
print(f"[llama.cpp] latest-tag lookup failed ({e}); pinned fallback {FALLBACK_TAG}")
return FALLBACK_TAG
def ensure_llama_server() -> Path:
tag = resolve_tag()
root = Path.home() / ".cache" / "llamacpp" / tag
server = root / "build" / "bin" / "llama-server"
if not server.exists():
# llama.cpp Linux binaries are now distributed as .tar.gz (not .zip)
url = (f"https://github.com/ggml-org/llama.cpp/releases/download/"
f"{tag}/llama-{tag}-bin-ubuntu-x64.tar.gz")
print(f"[llama.cpp] downloading {url}")
req = urllib.request.Request(url, headers={"User-Agent": "hf-space"})
with urllib.request.urlopen(req, timeout=180) as r:
blob = r.read()
root.mkdir(parents=True, exist_ok=True)
# Extract tar.gz archive
with tarfile.open(fileobj=io.BytesIO(blob), mode="r:gz") as t:
t.extractall(root)
for f in (root / "build" / "bin").iterdir():
f.chmod(0o755)
return server
# ===== 4. MODEL =====
print(f"[model] downloading {MODEL_REPO}/{MODEL_FILE}")
model_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)
# ===== 5. LAUNCH llama-server, SIZED TO THE DETECTED HARDWARE =====
server_bin = ensure_llama_server()
UPSTREAM = f"http://127.0.0.1:{LLAMA_PORT}"
env = os.environ.copy()
env["LD_LIBRARY_PATH"] = f"{server_bin.parent}:{env.get('LD_LIBRARY_PATH', '')}"
env.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
cmd = [
str(server_bin), "-m", model_path,
"--host", "127.0.0.1", "--port", str(LLAMA_PORT),
"-t", str(CORES), "-tb", str(CORES), # every core the cgroup gives us
"-c", str(CTX_SIZE),
"-np", str(PARALLEL), # real concurrent slots, no global lock
"-fa", # flash attention β†’ much smaller KV cache
"--cache-reuse", "256", # KV reuse across chat turns
]
if API_KEY:
cmd += ["--api-key", API_KEY]
proc = subprocess.Popen(cmd, env=env)
def wait_ready(timeout=600):
t0 = time.time()
while time.time() - t0 < timeout:
if proc.poll() is not None:
raise RuntimeError(f"llama-server died early (code {proc.returncode})")
try:
if httpx.get(f"{UPSTREAM}/health", timeout=2).status_code == 200:
print("[llama.cpp] server ready")
return
except Exception:
pass
time.sleep(1)
raise TimeoutError("llama-server not ready in time")
wait_ready()
# ===== 6. GRADIO UI (same backend as the API) =====
from openai import OpenAI
oai = OpenAI(base_url=f"{UPSTREAM}/v1", api_key=API_KEY or "not-needed")
def _normalize(history):
msgs = [{"role": "system", "content": SYSTEM_PROMPT}]
for item in history:
if isinstance(item, dict):
msgs.append({"role": item["role"], "content": item["content"]})
else:
user, bot = item
msgs.append({"role": "user", "content": user})
if bot:
msgs.append({"role": "assistant", "content": bot})
return msgs
def chat(message, history):
stream = oai.chat.completions.create(
model="local",
messages=_normalize(history) + [{"role": "user", "content": message}],
stream=True, temperature=0.7, max_tokens=1024,
)
out = ""
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
out += delta
yield out
with gr.Blocks() as demo:
gr.Markdown(
f"# ⚑ llama.cpp on ZeroGPU's free CPU\n"
f"`{MODEL_REPO}` Β· {CORES} threads Β· ctx {CTX_SIZE} Β· {PARALLEL} parallel slots\n\n"
f"**External API:** `POST /v1/chat/completions` on this Space's `*.hf.space` URL."
)
gr.ChatInterface(fn=chat, examples=["Who are you?",
"Write a python script to reverse a string.", "Explain quantum computing."])
demo = demo.queue()
# ===== 7. PUBLIC /v1 PROXY β€” the supported way, no monkey-patch =====
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import Response, StreamingResponse
app = FastAPI(title="llama-cpp-proxy")
app.add_middleware(CORSMiddleware, allow_origins=["*"],
allow_methods=["*"], allow_headers=["*"])
_http = httpx.AsyncClient(timeout=httpx.Timeout(None))
@app.api_route("/v1/{path:path}", methods=["GET", "POST"])
async def v1_proxy(path: str, request: Request):
body = await request.body()
headers = {k: request.headers[k] for k in ("authorization", "content-type")
if k in request.headers}
wants_stream = False
if body:
try:
wants_stream = bool(json.loads(body).get("stream"))
except Exception:
pass
url = f"{UPSTREAM}/v1/{path}"
if request.method == "POST" and wants_stream:
req = _http.build_request("POST", url, content=body, headers=headers)
resp = await _http.send(req, stream=True)
async def gen():
try:
async for chunk in resp.aiter_raw():
yield chunk
finally:
await resp.aclose()
return StreamingResponse(gen(), status_code=resp.status_code,
media_type="text/event-stream")
resp = await _http.request(request.method, url, content=body or None, headers=headers)
return Response(content=resp.content, status_code=resp.status_code,
media_type=resp.headers.get("content-type", "application/json"))
app = gr.mount_gradio_app(app, demo, path="/") # mounted LAST so /v1/* wins
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "7860")))