0xarchit commited on
Commit
8ebf335
·
1 Parent(s): 957406c

Add optimized llama.cpp CPU inference backend

Browse files
Files changed (7) hide show
  1. .dockerignore +9 -0
  2. .gitignore +1 -0
  3. Dockerfile +62 -0
  4. README.md +97 -1
  5. download_model.py +99 -0
  6. requirements.txt +1 -0
  7. 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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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[@]}"