File size: 8,037 Bytes
5e23e7e
693561f
 
38a6be8
7319988
693561f
2b52af6
da0d943
693561f
 
 
 
 
 
 
 
 
 
 
da0d943
693561f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5e23e7e
693561f
5e23e7e
693561f
 
 
 
 
5e23e7e
 
 
693561f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e2bbde
693561f
2b52af6
693561f
 
 
 
38a6be8
 
693561f
 
 
 
7319988
693561f
 
 
 
38a6be8
693561f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98a8b03
693561f
 
 
 
 
 
 
 
 
 
 
 
 
38a6be8
693561f
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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")))