Spaces:
Paused
Paused
THOX Rust Coder: Qwen3-Coder-REAP-25B-A3B-Rust GGUF via conda-forge llama.cpp
Browse files- Dockerfile +33 -0
- README.md +48 -5
- app.py +193 -0
- requirements.txt +14 -0
Dockerfile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# THOX Rust Coder — llama.cpp via the conda-forge PREBUILT binary.
|
| 2 |
+
#
|
| 3 |
+
# Do not try to compile llama-cpp-python on an HF Space. It does not fit the
|
| 4 |
+
# build timeout -- not in the Spaces pip step, and not in a Docker builder stage
|
| 5 |
+
# either, even at -j16 with tests/examples/server/tools disabled. Docker buys a
|
| 6 |
+
# glibc-linked extension, not build time. conda-forge ships a prebuilt linux-64
|
| 7 |
+
# glibc binary: correct ABI, no compile, ~2 minute build.
|
| 8 |
+
FROM mambaorg/micromamba:2.9-debian12
|
| 9 |
+
|
| 10 |
+
USER root
|
| 11 |
+
RUN mkdir -p /app && chown 1000:1000 /app
|
| 12 |
+
USER $MAMBA_USER
|
| 13 |
+
|
| 14 |
+
RUN micromamba install -y -n base -c conda-forge \
|
| 15 |
+
python=3.11 \
|
| 16 |
+
pip \
|
| 17 |
+
llama-cpp-python=0.3.34 \
|
| 18 |
+
&& micromamba clean --all --yes
|
| 19 |
+
|
| 20 |
+
ARG MAMBA_DOCKERFILE_ACTIVATE=1
|
| 21 |
+
|
| 22 |
+
COPY --chown=$MAMBA_USER:$MAMBA_USER requirements.txt /tmp/requirements.txt
|
| 23 |
+
RUN pip install --no-cache-dir -r /tmp/requirements.txt
|
| 24 |
+
|
| 25 |
+
ENV HF_HOME=/home/mambauser/.cache/huggingface \
|
| 26 |
+
GRADIO_SERVER_NAME=0.0.0.0 \
|
| 27 |
+
GRADIO_SERVER_PORT=7860
|
| 28 |
+
|
| 29 |
+
WORKDIR /app
|
| 30 |
+
COPY --chown=$MAMBA_USER:$MAMBA_USER app.py /app/app.py
|
| 31 |
+
|
| 32 |
+
EXPOSE 7860
|
| 33 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,53 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: THOX Rust Coder
|
| 3 |
+
emoji: 🦀
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: red
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
+
license: apache-2.0
|
| 10 |
+
models:
|
| 11 |
+
- Em-80/Qwen3-coder-REAP-25B-A3B-Rust-GGUF
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# THOX Rust Coder — Qwen3-Coder-REAP-25B-A3B-Rust
|
| 15 |
+
|
| 16 |
+
Hosted Rust coding model, served as GGUF through llama.cpp.
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
curl -s -X POST https://thox-ai-thoxrustcoder.hf.space/v1/chat/completions \
|
| 20 |
+
-H 'Content-Type: application/json' \
|
| 21 |
+
-d '{"messages":[{"role":"user","content":"Write a Rust fn to parse semver."}],
|
| 22 |
+
"max_tokens":256}'
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
`GET /healthz` · `POST /v1/chat/completions` (OpenAI-shaped, ThoxRoute-registerable)
|
| 26 |
+
|
| 27 |
+
## "3B active" is not a memory budget
|
| 28 |
+
|
| 29 |
+
This is MoE: **25B total, ~3B active per token.** The two numbers govern
|
| 30 |
+
different resources, and conflating them leads to picking a tier that cannot
|
| 31 |
+
load the model:
|
| 32 |
+
|
| 33 |
+
| | sized by |
|
| 34 |
+
|---|---|
|
| 35 |
+
| **memory** | **25B total** — every expert stays resident, since the router may pick any of them on any token |
|
| 36 |
+
| **compute** | **~3B active** — decode costs about what a 3B dense model costs |
|
| 37 |
+
|
| 38 |
+
Q4_K_M is therefore **15.1 GB**, not "3B-worth". What MoE buys is speed per
|
| 39 |
+
resident byte — which is precisely what makes a CPU tier viable: 25B-sized
|
| 40 |
+
memory, 3B-sized arithmetic.
|
| 41 |
+
|
| 42 |
+
Runs on `cpu-upgrade` (8 vCPU / 32 GB). Throughput is measured and reported in
|
| 43 |
+
every response under `thox_perf`, and in the UI — no throughput claim appears in
|
| 44 |
+
this README that was not taken from this Space.
|
| 45 |
+
|
| 46 |
+
## License — verified, not assumed
|
| 47 |
+
|
| 48 |
+
| repo | license | checked |
|
| 49 |
+
|---|---|---|
|
| 50 |
+
| `Em-80/Qwen3-coder-REAP-25B-A3B-Rust-GGUF` | **Apache-2.0** | ungated, public |
|
| 51 |
+
|
| 52 |
+
Apache-2.0 permits hosting, commercial use and redistribution. Verified against
|
| 53 |
+
the model repo's own metadata before this Space was published.
|
app.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""THOX Rust Coder — hosted Qwen3-Coder-REAP-25B-A3B-Rust (GGUF).
|
| 2 |
+
|
| 3 |
+
SIZING: "3B active" IS NOT A MEMORY BUDGET
|
| 4 |
+
------------------------------------------
|
| 5 |
+
This is a Mixture-of-Experts model: 25B total parameters, ~3B active per token.
|
| 6 |
+
Those two numbers govern different resources and it is easy to conflate them:
|
| 7 |
+
|
| 8 |
+
active params (~3B) -> COMPUTE per token. Decode is as cheap as a 3B dense.
|
| 9 |
+
total params (25B) -> MEMORY. Every expert must be resident, because the
|
| 10 |
+
router may select any of them on any token.
|
| 11 |
+
|
| 12 |
+
So a Q4_K_M GGUF is **15.1 GB of RAM/VRAM**, not "3B-worth". You cannot fit this
|
| 13 |
+
on a tier sized for a 3B model. What MoE buys you here is speed-per-byte, not a
|
| 14 |
+
smaller footprint -- which is exactly why a CPU tier is viable at all: we pay
|
| 15 |
+
25B-sized memory but only 3B-sized arithmetic.
|
| 16 |
+
|
| 17 |
+
TIER
|
| 18 |
+
----
|
| 19 |
+
Starts on `cpu-upgrade` (8 vCPU / 32 GB, ~$0.03/hr). That fits Q4_K_M with room
|
| 20 |
+
for the KV cache, and the 3B active path keeps CPU decode tolerable. If measured
|
| 21 |
+
throughput is too slow to be a useful coding assistant, escalate to a GPU tier --
|
| 22 |
+
but escalate on a MEASUREMENT, not on the assumption that 25B implies a GPU.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import os
|
| 28 |
+
import time
|
| 29 |
+
import uuid
|
| 30 |
+
|
| 31 |
+
import gradio as gr
|
| 32 |
+
from fastapi import FastAPI
|
| 33 |
+
from huggingface_hub import hf_hub_download
|
| 34 |
+
from pydantic import BaseModel
|
| 35 |
+
|
| 36 |
+
MODEL_REPO = os.environ.get("THOX_MODEL_REPO", "Em-80/Qwen3-coder-REAP-25B-A3B-Rust-GGUF")
|
| 37 |
+
MODEL_FILE = os.environ.get("THOX_MODEL_FILE", "Qwen3-Coder-REAP-25B-A3B-Rust-Q4_K_M.gguf")
|
| 38 |
+
N_CTX = int(os.environ.get("THOX_N_CTX", "8192"))
|
| 39 |
+
|
| 40 |
+
SYSTEM = (
|
| 41 |
+
"You are THOX Rust Coder. You write correct, idiomatic Rust. Prefer showing "
|
| 42 |
+
"compiling code over prose. If a request is ambiguous, state the assumption "
|
| 43 |
+
"you made in one line, then give the code."
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
_llm = None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _usable_cpus() -> int:
|
| 50 |
+
"""Threads from the cgroup quota, not the host.
|
| 51 |
+
|
| 52 |
+
`os.cpu_count()` reports the HOST's core count inside a container. On a
|
| 53 |
+
sibling Space this oversubscribed a 2-vCPU cgroup ~8x and cost ~290x
|
| 54 |
+
throughput -- the model ran slower than the edge device it was meant to
|
| 55 |
+
offload. Read the quota.
|
| 56 |
+
"""
|
| 57 |
+
try:
|
| 58 |
+
quota, period = open("/sys/fs/cgroup/cpu.max").read().split()
|
| 59 |
+
if quota != "max":
|
| 60 |
+
return max(1, int(int(quota) / int(period)))
|
| 61 |
+
except Exception:
|
| 62 |
+
pass
|
| 63 |
+
try:
|
| 64 |
+
q = int(open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us").read())
|
| 65 |
+
p = int(open("/sys/fs/cgroup/cpu/cpu.cfs_period_us").read())
|
| 66 |
+
if q > 0:
|
| 67 |
+
return max(1, q // p)
|
| 68 |
+
except Exception:
|
| 69 |
+
pass
|
| 70 |
+
try:
|
| 71 |
+
return max(1, len(os.sched_getaffinity(0)))
|
| 72 |
+
except Exception:
|
| 73 |
+
return os.cpu_count() or 2
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def llm():
|
| 77 |
+
global _llm
|
| 78 |
+
if _llm is None:
|
| 79 |
+
from llama_cpp import Llama
|
| 80 |
+
|
| 81 |
+
path = hf_hub_download(MODEL_REPO, MODEL_FILE,
|
| 82 |
+
token=os.environ.get("HF_TOKEN"))
|
| 83 |
+
_llm = Llama(
|
| 84 |
+
model_path=path,
|
| 85 |
+
n_ctx=N_CTX,
|
| 86 |
+
n_threads=_usable_cpus(),
|
| 87 |
+
# -1 offloads every layer when a GPU is present, and is simply
|
| 88 |
+
# ignored on a CPU build -- so the same image works on both tiers.
|
| 89 |
+
n_gpu_layers=int(os.environ.get("THOX_GPU_LAYERS", "-1")),
|
| 90 |
+
verbose=False,
|
| 91 |
+
)
|
| 92 |
+
return _llm
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def generate(messages, max_tokens=512, temperature=0.2):
|
| 96 |
+
t0 = time.time()
|
| 97 |
+
# create_chat_completion uses the chat template embedded in the GGUF, rather
|
| 98 |
+
# than a hand-rolled one. Qwen3-Coder is ChatML, but reading it from the file
|
| 99 |
+
# means a re-quant with a different template does not silently break output.
|
| 100 |
+
out = llm().create_chat_completion(
|
| 101 |
+
messages=messages, max_tokens=max_tokens, temperature=temperature,
|
| 102 |
+
)
|
| 103 |
+
dt = time.time() - t0
|
| 104 |
+
text = out["choices"][0]["message"]["content"]
|
| 105 |
+
n = out.get("usage", {}).get("completion_tokens") or 0
|
| 106 |
+
return text, n, dt
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
api = FastAPI()
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class Msg(BaseModel):
|
| 113 |
+
role: str
|
| 114 |
+
content: str
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
class ChatRequest(BaseModel):
|
| 118 |
+
model: str | None = None
|
| 119 |
+
messages: list[Msg]
|
| 120 |
+
max_tokens: int | None = 512
|
| 121 |
+
temperature: float | None = 0.2
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@api.get("/healthz")
|
| 125 |
+
def healthz():
|
| 126 |
+
return {
|
| 127 |
+
"status": "ok",
|
| 128 |
+
"model": MODEL_REPO,
|
| 129 |
+
"file": MODEL_FILE,
|
| 130 |
+
"role": "thox-rust-coder",
|
| 131 |
+
"n_ctx": N_CTX,
|
| 132 |
+
"threads": _usable_cpus(),
|
| 133 |
+
"loaded": _llm is not None,
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@api.post("/v1/chat/completions")
|
| 138 |
+
def chat_completions(req: ChatRequest):
|
| 139 |
+
msgs = [m.model_dump() for m in req.messages]
|
| 140 |
+
if not any(m["role"] == "system" for m in msgs):
|
| 141 |
+
msgs = [{"role": "system", "content": SYSTEM}] + msgs
|
| 142 |
+
text, n, dt = generate(msgs, req.max_tokens or 512, req.temperature or 0.2)
|
| 143 |
+
return {
|
| 144 |
+
"id": "chatcmpl-" + uuid.uuid4().hex[:12],
|
| 145 |
+
"object": "chat.completion",
|
| 146 |
+
"created": int(time.time()),
|
| 147 |
+
"model": "thox-rust-coder",
|
| 148 |
+
"choices": [{"index": 0, "finish_reason": "stop",
|
| 149 |
+
"message": {"role": "assistant", "content": text}}],
|
| 150 |
+
"usage": {"completion_tokens": n},
|
| 151 |
+
"thox_perf": {"tok_per_s": round(n / dt, 1) if dt else None,
|
| 152 |
+
"seconds": round(dt, 2)},
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def ui(prompt, max_tokens):
|
| 157 |
+
text, n, dt = generate(
|
| 158 |
+
[{"role": "system", "content": SYSTEM}, {"role": "user", "content": prompt}],
|
| 159 |
+
int(max_tokens),
|
| 160 |
+
)
|
| 161 |
+
tps = n / dt if dt else 0
|
| 162 |
+
return text, f"{n} tok in {dt:.1f}s = {tps:.1f} tok/s"
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
demo = gr.Interface(
|
| 166 |
+
fn=ui,
|
| 167 |
+
inputs=[gr.Textbox(label="Prompt", lines=4,
|
| 168 |
+
value="Write a Rust function that parses a semver string "
|
| 169 |
+
"into (major, minor, patch), returning Result."),
|
| 170 |
+
gr.Slider(64, 2048, value=512, step=64, label="max tokens")],
|
| 171 |
+
outputs=[gr.Code(label="THOX Rust Coder", language="rust"),
|
| 172 |
+
gr.Textbox(label="Measured")],
|
| 173 |
+
title="THOX Rust Coder — Qwen3-Coder-REAP-25B-A3B-Rust",
|
| 174 |
+
description=(
|
| 175 |
+
"Rust-specialised MoE coder, Apache-2.0, served as GGUF.\n\n"
|
| 176 |
+
"`POST /v1/chat/completions` (OpenAI-shaped, ThoxRoute-registerable) · "
|
| 177 |
+
"`GET /healthz`\n\n"
|
| 178 |
+
"**25B total / ~3B active.** Memory is sized by the 25B (all experts stay "
|
| 179 |
+
"resident); speed is sized by the 3B. That combination is what makes a "
|
| 180 |
+
"CPU tier viable."
|
| 181 |
+
),
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
app = gr.mount_gradio_app(api, demo, path="/")
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
if __name__ == "__main__":
|
| 188 |
+
# Defining `app` does not serve it -- without this the process exits 0 and
|
| 189 |
+
# the Space reports RUNTIME_ERROR with no traceback to read.
|
| 190 |
+
import uvicorn
|
| 191 |
+
|
| 192 |
+
uvicorn.run(app, host="0.0.0.0",
|
| 193 |
+
port=int(os.environ.get("GRADIO_SERVER_PORT", 7860)))
|
requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# llama-cpp-python is deliberately ABSENT.
|
| 2 |
+
#
|
| 3 |
+
# It is installed from conda-forge in the Dockerfile as a prebuilt, glibc-linked
|
| 4 |
+
# linux-64 binary. Two pip routes both fail on an HF Space:
|
| 5 |
+
# * PyPI is sdist-only -> compiles -> `Job timeout`
|
| 6 |
+
# * abetlen's CPU wheel index -> musl-linked -> builds green, RUNNING, /healthz
|
| 7 |
+
# ok, then HTTP 500 at the first dlopen
|
| 8 |
+
# See thoxllm-factory/docs/SPACE_PINS.md.
|
| 9 |
+
|
| 10 |
+
gradio==5.50.0
|
| 11 |
+
fastapi
|
| 12 |
+
pydantic>=2
|
| 13 |
+
uvicorn
|
| 14 |
+
huggingface_hub>=0.28
|