Add optimized llama.cpp CPU inference backend
Browse files- .dockerignore +9 -0
- .gitignore +1 -0
- Dockerfile +62 -0
- README.md +97 -1
- download_model.py +99 -0
- requirements.txt +1 -0
- start.sh +39 -0
.dockerignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.venv
|
| 3 |
+
__pycache__
|
| 4 |
+
*.pyc
|
| 5 |
+
*.pyo
|
| 6 |
+
*.pyd
|
| 7 |
+
.env
|
| 8 |
+
models
|
| 9 |
+
hf-cache
|
.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
**/*.env
|
Dockerfile
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM debian:bookworm-slim AS builder
|
| 2 |
+
|
| 3 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 6 |
+
git \
|
| 7 |
+
build-essential \
|
| 8 |
+
cmake \
|
| 9 |
+
libopenblas-dev \
|
| 10 |
+
ca-certificates \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
WORKDIR /opt
|
| 14 |
+
RUN git clone --depth 1 https://github.com/ggml-org/llama.cpp.git
|
| 15 |
+
|
| 16 |
+
WORKDIR /opt/llama.cpp
|
| 17 |
+
RUN cmake -B build -S . \
|
| 18 |
+
-DGGML_BLAS=ON \
|
| 19 |
+
-DGGML_BLAS_VENDOR=OpenBLAS \
|
| 20 |
+
-DGGML_NATIVE=ON \
|
| 21 |
+
-DCMAKE_BUILD_TYPE=Release \
|
| 22 |
+
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \
|
| 23 |
+
-DCMAKE_C_FLAGS_RELEASE="-Ofast -march=native -flto" \
|
| 24 |
+
-DCMAKE_CXX_FLAGS_RELEASE="-Ofast -march=native -flto" \
|
| 25 |
+
&& cmake --build build -j"$(nproc)" --target llama-server
|
| 26 |
+
|
| 27 |
+
FROM debian:bookworm-slim
|
| 28 |
+
|
| 29 |
+
ENV DEBIAN_FRONTEND=noninteractive \
|
| 30 |
+
PYTHONUNBUFFERED=1 \
|
| 31 |
+
HF_HUB_DISABLE_TELEMETRY=1 \
|
| 32 |
+
HF_HOME=/data/hf-cache \
|
| 33 |
+
MODEL_DIR=/data/models \
|
| 34 |
+
MODEL_PATH=/data/models/model.gguf
|
| 35 |
+
|
| 36 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 37 |
+
python3 \
|
| 38 |
+
python3-pip \
|
| 39 |
+
ca-certificates \
|
| 40 |
+
libopenblas0-pthread \
|
| 41 |
+
libgomp1 \
|
| 42 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 43 |
+
|
| 44 |
+
COPY --from=builder /opt/llama.cpp/build/bin/llama-server /usr/local/bin/llama-server
|
| 45 |
+
|
| 46 |
+
WORKDIR /app
|
| 47 |
+
COPY requirements.txt /app/requirements.txt
|
| 48 |
+
RUN python3 -m pip install --no-cache-dir --upgrade pip \
|
| 49 |
+
&& python3 -m pip install --no-cache-dir -r /app/requirements.txt \
|
| 50 |
+
&& rm -rf /root/.cache/pip
|
| 51 |
+
|
| 52 |
+
COPY download_model.py /app/download_model.py
|
| 53 |
+
COPY start.sh /app/start.sh
|
| 54 |
+
RUN chmod +x /app/start.sh \
|
| 55 |
+
&& mkdir -p /data/models /data/hf-cache \
|
| 56 |
+
&& chown -R 65532:65532 /app /data
|
| 57 |
+
|
| 58 |
+
EXPOSE 7860
|
| 59 |
+
|
| 60 |
+
USER 65532:65532
|
| 61 |
+
|
| 62 |
+
ENTRYPOINT ["/app/start.sh"]
|
README.md
CHANGED
|
@@ -7,4 +7,100 @@ sdk: docker
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# CPU Inference Backend
|
| 11 |
+
|
| 12 |
+
This Space is a backend-only OpenAI-compatible inference server built on `llama.cpp` and optimized for CPU-only Hugging Face Docker Spaces.
|
| 13 |
+
|
| 14 |
+
It automatically downloads the target Hugging Face model from `MODEL_NAME`, stores it under `/data/models/model.gguf`, and starts `llama-server` with OpenAI-compatible endpoints.
|
| 15 |
+
|
| 16 |
+
## Environment variables
|
| 17 |
+
|
| 18 |
+
Set these in the Space settings:
|
| 19 |
+
|
| 20 |
+
- `MODEL_NAME`: Hugging Face repo id that contains one or more GGUF files
|
| 21 |
+
- `HF_TOKEN`: Hugging Face token used for the download
|
| 22 |
+
- `API_PASSWORD`: bearer token required by the API
|
| 23 |
+
|
| 24 |
+
Optional tuning variables:
|
| 25 |
+
|
| 26 |
+
- `CTX_SIZE`: context size, default `4096`
|
| 27 |
+
- `THREADS`: CPU thread count, default `2`
|
| 28 |
+
- `THREADS_BATCH`: batch thread count, default matches `THREADS`
|
| 29 |
+
- `BATCH_SIZE`: prompt batch size, default `512`
|
| 30 |
+
- `UBATCH_SIZE`: micro-batch size, default `512`
|
| 31 |
+
- `CACHE_TYPE_K`: KV cache type for keys, default `q8_0`
|
| 32 |
+
- `CACHE_TYPE_V`: KV cache type for values, default `q8_0`
|
| 33 |
+
- `PARALLEL`: optional override for concurrent prompt decode slots
|
| 34 |
+
- `PORT`: listen port, default `7860`
|
| 35 |
+
|
| 36 |
+
## Endpoints
|
| 37 |
+
|
| 38 |
+
The server exposes:
|
| 39 |
+
|
| 40 |
+
- `/v1/chat/completions`
|
| 41 |
+
- `/v1/completions`
|
| 42 |
+
- `/v1/models`
|
| 43 |
+
|
| 44 |
+
Streaming is supported by `llama-server`. Bearer token authentication uses `--api-key` directly.
|
| 45 |
+
|
| 46 |
+
## Build and runtime
|
| 47 |
+
|
| 48 |
+
The Docker image uses a multi-stage build on Ubuntu 24.04. The builder stage installs `git`, `build-essential`, `cmake`, and `libopenblas-dev`, clones `llama.cpp`, and compiles `llama-server` with `-Ofast`, `-march=native`, `-flto`, OpenBLAS, and native CPU optimizations. The runtime stage keeps only Python, OpenBLAS runtime libraries, the compiled server, and the downloader.
|
| 49 |
+
|
| 50 |
+
Model downloads and HF cache live on the `/data` bucket so restarts do not redownload the model.
|
| 51 |
+
|
| 52 |
+
## curl examples
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
curl http://localhost:7860/v1/models \
|
| 56 |
+
-H "Authorization: Bearer $API_PASSWORD"
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
curl http://localhost:7860/v1/chat/completions \
|
| 61 |
+
-H "Authorization: Bearer $API_PASSWORD" \
|
| 62 |
+
-H "Content-Type: application/json" \
|
| 63 |
+
-d '{
|
| 64 |
+
"model": "model",
|
| 65 |
+
"messages": [
|
| 66 |
+
{"role": "user", "content": "Write a one-sentence summary of llama.cpp."}
|
| 67 |
+
],
|
| 68 |
+
"stream": false
|
| 69 |
+
}'
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
curl http://localhost:7860/v1/completions \
|
| 74 |
+
-H "Authorization: Bearer $API_PASSWORD" \
|
| 75 |
+
-H "Content-Type: application/json" \
|
| 76 |
+
-d '{
|
| 77 |
+
"model": "model",
|
| 78 |
+
"prompt": "Explain KV cache in one paragraph.",
|
| 79 |
+
"max_tokens": 128,
|
| 80 |
+
"stream": false
|
| 81 |
+
}'
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
## OpenAI SDK example
|
| 85 |
+
|
| 86 |
+
```python
|
| 87 |
+
import os
|
| 88 |
+
|
| 89 |
+
from openai import OpenAI
|
| 90 |
+
|
| 91 |
+
client = OpenAI(
|
| 92 |
+
base_url="http://localhost:7860/v1",
|
| 93 |
+
api_key=os.environ["API_PASSWORD"],
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
response = client.chat.completions.create(
|
| 97 |
+
model="model",
|
| 98 |
+
messages=[{"role": "user", "content": "Hello"}],
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
print(response.choices[0].message.content)
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## Hugging Face Spaces notes
|
| 105 |
+
|
| 106 |
+
This repository is ready for a Docker Space with no frontend. The only required changes at deployment time are the three environment variables above.
|
download_model.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import shutil
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from huggingface_hub import snapshot_download
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def require_env(name: str) -> str:
|
| 11 |
+
value = os.environ.get(name, "").strip()
|
| 12 |
+
if not value:
|
| 13 |
+
raise SystemExit(f"missing required environment variable: {name}")
|
| 14 |
+
return value
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def quant_rank(path: Path) -> tuple[int, int, str]:
|
| 18 |
+
name = path.name.lower()
|
| 19 |
+
size = path.stat().st_size
|
| 20 |
+
|
| 21 |
+
q8_tokens = ("q8_k_l", "q8_k_m", "q8_k_s", "q8_k", "q8_0", "q8_1")
|
| 22 |
+
if any(token in name for token in q8_tokens):
|
| 23 |
+
return (0, -size, path.name)
|
| 24 |
+
|
| 25 |
+
if "f16" in name or "bf16" in name:
|
| 26 |
+
return (1, -size, path.name)
|
| 27 |
+
|
| 28 |
+
if any(token in name for token in ("q6_k", "q6_0", "q6_1", "q6")):
|
| 29 |
+
return (2, -size, path.name)
|
| 30 |
+
|
| 31 |
+
if any(token in name for token in ("q5_k_m", "q5_k_s", "q5_k", "q5_0", "q5_1", "q5")):
|
| 32 |
+
return (3, -size, path.name)
|
| 33 |
+
|
| 34 |
+
if any(token in name for token in ("q4_k_m", "q4_k_s", "q4_k", "q4_0", "q4_1", "q4")):
|
| 35 |
+
return (4, -size, path.name)
|
| 36 |
+
|
| 37 |
+
if any(token in name for token in ("q3_k_m", "q3_k_s", "q3_k", "q3_0", "q3_1", "q3")):
|
| 38 |
+
return (5, -size, path.name)
|
| 39 |
+
|
| 40 |
+
if any(token in name for token in ("q2_k", "q2_0", "q2_1", "q2")):
|
| 41 |
+
return (6, -size, path.name)
|
| 42 |
+
|
| 43 |
+
if "f32" in name:
|
| 44 |
+
return (7, -size, path.name)
|
| 45 |
+
|
| 46 |
+
return (8, -size, path.name)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def main() -> None:
|
| 50 |
+
model_name = require_env("MODEL_NAME")
|
| 51 |
+
hf_token = require_env("HF_TOKEN")
|
| 52 |
+
require_env("API_PASSWORD")
|
| 53 |
+
|
| 54 |
+
models_dir = Path(os.environ.get("MODEL_DIR", "/data/models"))
|
| 55 |
+
models_dir.mkdir(parents=True, exist_ok=True)
|
| 56 |
+
|
| 57 |
+
target_model = models_dir / "model.gguf"
|
| 58 |
+
repo_marker = models_dir / "model.repo"
|
| 59 |
+
download_dir = models_dir / ".download"
|
| 60 |
+
|
| 61 |
+
if target_model.exists() and repo_marker.exists() and repo_marker.read_text(encoding="utf-8").strip() == model_name:
|
| 62 |
+
print(f"using cached model: {target_model}")
|
| 63 |
+
return
|
| 64 |
+
|
| 65 |
+
if download_dir.exists():
|
| 66 |
+
shutil.rmtree(download_dir)
|
| 67 |
+
download_dir.mkdir(parents=True, exist_ok=True)
|
| 68 |
+
|
| 69 |
+
print(f"downloading model: {model_name}")
|
| 70 |
+
snapshot_download(
|
| 71 |
+
repo_id=model_name,
|
| 72 |
+
token=hf_token,
|
| 73 |
+
allow_patterns=["*.gguf"],
|
| 74 |
+
local_dir=str(download_dir),
|
| 75 |
+
local_dir_use_symlinks=False,
|
| 76 |
+
cache_dir=os.environ.get("HF_HOME", "/data/hf-cache"),
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
candidates = [path for path in download_dir.rglob("*.gguf") if path.is_file()]
|
| 80 |
+
if not candidates:
|
| 81 |
+
raise SystemExit(f"no gguf files found in repository: {model_name}")
|
| 82 |
+
|
| 83 |
+
selected = min(candidates, key=quant_rank)
|
| 84 |
+
print(f"selected model: {selected.name}")
|
| 85 |
+
|
| 86 |
+
if target_model.exists():
|
| 87 |
+
target_model.unlink()
|
| 88 |
+
shutil.move(str(selected), str(target_model))
|
| 89 |
+
|
| 90 |
+
if repo_marker.exists():
|
| 91 |
+
repo_marker.unlink()
|
| 92 |
+
repo_marker.write_text(model_name, encoding="utf-8")
|
| 93 |
+
|
| 94 |
+
shutil.rmtree(download_dir, ignore_errors=True)
|
| 95 |
+
print(f"ready: {target_model}")
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
if __name__ == "__main__":
|
| 99 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
huggingface_hub>=0.24,<1
|
start.sh
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
THREADS="${THREADS:-$(nproc)}"
|
| 5 |
+
export OMP_NUM_THREADS="$THREADS"
|
| 6 |
+
export OPENBLAS_NUM_THREADS="$THREADS"
|
| 7 |
+
export MKL_NUM_THREADS="$THREADS"
|
| 8 |
+
export VECLIB_MAXIMUM_THREADS="$THREADS"
|
| 9 |
+
export NUMEXPR_NUM_THREADS="$THREADS"
|
| 10 |
+
export HF_HOME="${HF_HOME:-/data/hf-cache}"
|
| 11 |
+
export MODEL_DIR="${MODEL_DIR:-/data/models}"
|
| 12 |
+
export MODEL_PATH="${MODEL_PATH:-/data/models/model.gguf}"
|
| 13 |
+
|
| 14 |
+
python3 /app/download_model.py
|
| 15 |
+
|
| 16 |
+
LLAMA_SERVER_BIN="${LLAMA_SERVER_BIN:-/usr/local/bin/llama-server}"
|
| 17 |
+
|
| 18 |
+
server_args=(
|
| 19 |
+
--model "$MODEL_PATH"
|
| 20 |
+
--host 0.0.0.0
|
| 21 |
+
--port "${PORT:-7860}"
|
| 22 |
+
--api-key "$API_PASSWORD"
|
| 23 |
+
--mmap
|
| 24 |
+
-ngl 0
|
| 25 |
+
-t "$THREADS"
|
| 26 |
+
--threads-batch "${THREADS_BATCH:-$THREADS}"
|
| 27 |
+
--batch-size "${BATCH_SIZE:-512}"
|
| 28 |
+
--ubatch-size "${UBATCH_SIZE:-512}"
|
| 29 |
+
--ctx-size "${CTX_SIZE:-4096}"
|
| 30 |
+
--cache-type-k "${CACHE_TYPE_K:-f16}"
|
| 31 |
+
--cache-type-v "${CACHE_TYPE_V:-f16}"
|
| 32 |
+
--metrics
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
if [[ -n "${PARALLEL:-}" ]]; then
|
| 36 |
+
server_args+=(--parallel "$PARALLEL")
|
| 37 |
+
fi
|
| 38 |
+
|
| 39 |
+
exec "$LLAMA_SERVER_BIN" "${server_args[@]}"
|