devarshia5 commited on
Commit
ed7c75c
·
verified ·
1 Parent(s): d095bd1

Upload 7 files

Browse files
Files changed (6) hide show
  1. Dockerfile +9 -6
  2. README.md +21 -9
  3. app.py +28 -10
  4. documents/sample.txt +5 -3
  5. index.html +2 -2
  6. requirements.txt +8 -5
Dockerfile CHANGED
@@ -1,9 +1,12 @@
1
  # CPU + FREE-tier RAG Space (Docker SDK).
2
- # Embeddings : BAAI/bge-small-en-v1.5 (fastembed / ONNX, no torch)
3
  # Vector DB : FAISS (in-memory)
4
- # LLM : Qwen2.5-Coder-0.5B-Instruct (llama.cpp GGUF; code-tuned, fast on CPU)
5
  # API : OpenAI-compatible -> /v1/chat/completions (+ web UI at /)
6
  #
 
 
 
7
  # Everything is CPU-only. No GPU, no paid hardware required.
8
  # Models are baked into the image so the Space cold-starts instantly.
9
 
@@ -36,10 +39,10 @@ ENV CMAKE_ARGS="-DGGML_NATIVE=OFF -DGGML_AVX=ON -DGGML_AVX2=ON -DGGML_FMA=ON -DG
36
  RUN pip install --no-cache-dir --user --no-binary=llama-cpp-python -r requirements.txt
37
 
38
  # --- Model config ------------------------------------------------------------
39
- # Code-tuned 0.5B: same size/speed as the generic 0.5B, but much better at
40
- # reading & explaining code. Q4_K_M is the CPU sweet spot.
41
- ENV LLM_REPO=Qwen/Qwen2.5-Coder-0.5B-Instruct-GGUF \
42
- LLM_FILE=qwen2.5-coder-0.5b-instruct-q4_k_m.gguf \
43
  MODEL_DIR=/home/user/models \
44
  EMBED_MODEL=BAAI/bge-small-en-v1.5 \
45
  FASTEMBED_CACHE=/home/user/.cache/fastembed \
 
1
  # CPU + FREE-tier RAG Space (Docker SDK).
2
+ # Embeddings : BAAI/bge-small-en-v1.5 (fastembed / ONNX, no torch)
3
  # Vector DB : FAISS (in-memory)
4
+ # LLM : Qwen3.5-0.8B (MoE) (llama.cpp GGUF; small + fast on CPU)
5
  # API : OpenAI-compatible -> /v1/chat/completions (+ web UI at /)
6
  #
7
+ # NOTE: Qwen3.5 uses the `qwen35` arch -> requires llama-cpp-python >= 0.3.32
8
+ # (llama.cpp >= b9616). See requirements.txt.
9
+ #
10
  # Everything is CPU-only. No GPU, no paid hardware required.
11
  # Models are baked into the image so the Space cold-starts instantly.
12
 
 
39
  RUN pip install --no-cache-dir --user --no-binary=llama-cpp-python -r requirements.txt
40
 
41
  # --- Model config ------------------------------------------------------------
42
+ # Qwen3.5-0.8B (MoE): tiny active-param count -> fast on CPU, punches above a
43
+ # dense 0.5B. Q4_K_M (~533 MB) is the CPU sweet spot. Text-only GGUF (no vision).
44
+ ENV LLM_REPO=unsloth/Qwen3.5-0.8B-GGUF \
45
+ LLM_FILE=Qwen3.5-0.8B-Q4_K_M.gguf \
46
  MODEL_DIR=/home/user/models \
47
  EMBED_MODEL=BAAI/bge-small-en-v1.5 \
48
  FASTEMBED_CACHE=/home/user/.cache/fastembed \
README.md CHANGED
@@ -9,7 +9,7 @@ pinned: false
9
  license: mit
10
  ---
11
 
12
- # CPU RAG Space — Qwen2.5-Coder-0.5B + FAISS (free tier, no GPU)
13
 
14
  A self-contained Retrieval-Augmented Generation service that runs **entirely on
15
  CPU** and fits the Hugging Face **free tier** (2 vCPU / 16 GB).
@@ -18,16 +18,20 @@ CPU** and fits the Hugging Face **free tier** (2 vCPU / 16 GB).
18
  |-----------|------|-----|
19
  | Embeddings | `BAAI/bge-small-en-v1.5` via `fastembed` (ONNX) | fast on CPU, no PyTorch, ~130 MB |
20
  | Vector DB | **FAISS** (in-memory) | tiny, instant search |
21
- | LLM | `Qwen2.5-Coder-0.5B-Instruct` GGUF Q4_K_M via llama.cpp | code-tuned, ~350 MB, fastest tier on 2 vCPU |
22
  | API | OpenAI-compatible `/v1/chat/completions` + web UI | drop-in for any client |
23
 
24
- Total footprint ≈ 0.7–1 GB RAM. Retrieval adds ~20 ms; the LLM is the only
25
  real latency.
26
 
 
 
 
 
27
  **Tuned for CPU speed:** 2K context (small KV cache), flash-attention, short
28
- 256-token answers, a startup warm-up, and a **streaming** web UI so the first
29
- word appears in ~1–1.5 s (decode runs ~5–6 tok/s on 2 vCPU). See
30
- `.github/workflows/keepalive.yml` for the keep-warm ping.
31
 
32
  ## Deploy (drag & drop)
33
 
@@ -68,9 +72,17 @@ Extra endpoints: `POST /ingest` (upload a doc), `GET /stats`.
68
  Change `LLM_REPO` / `LLM_FILE` in the `Dockerfile` (confirm exact filenames on
69
  the repo's *Files* tab):
70
 
71
- - Current (code-tuned, fastest): `Qwen/Qwen2.5-Coder-0.5B-Instruct-GGUF`
72
- - Smarter on code, ~½ the speed: `Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF`
73
- - Best general RAG at this size: `HuggingFaceTB/SmolLM2-1.7B-Instruct-GGUF`
 
 
 
 
 
 
 
 
74
 
75
  ## Keep it warm (avoid cold starts)
76
 
 
9
  license: mit
10
  ---
11
 
12
+ # CPU RAG Space — Qwen3.5-0.8B + FAISS (free tier, no GPU)
13
 
14
  A self-contained Retrieval-Augmented Generation service that runs **entirely on
15
  CPU** and fits the Hugging Face **free tier** (2 vCPU / 16 GB).
 
18
  |-----------|------|-----|
19
  | Embeddings | `BAAI/bge-small-en-v1.5` via `fastembed` (ONNX) | fast on CPU, no PyTorch, ~130 MB |
20
  | Vector DB | **FAISS** (in-memory) | tiny, instant search |
21
+ | LLM | `Qwen3.5-0.8B` (MoE) GGUF Q4_K_M via llama.cpp | ~533 MB, small MoE — fast on 2 vCPU, smarter than a dense 0.5B |
22
  | API | OpenAI-compatible `/v1/chat/completions` + web UI | drop-in for any client |
23
 
24
+ Total footprint ≈ 0.8–1.1 GB RAM. Retrieval adds ~20 ms; the LLM is the only
25
  real latency.
26
 
27
+ > **Runtime requirement:** Qwen3.5 uses the `qwen35` architecture, so this needs
28
+ > **`llama-cpp-python >= 0.3.32`** (llama.cpp ≥ b9616). The Dockerfile builds it
29
+ > from source. Older pins fail with *"unknown architecture 'qwen35'"*.
30
+
31
  **Tuned for CPU speed:** 2K context (small KV cache), flash-attention, short
32
+ 256-token answers, relevance-gated RAG (greetings/off-topic skip retrieval), a
33
+ startup warm-up, and a **streaming** web UI so the first word appears in ~1 s.
34
+ See `.github/workflows/keepalive.yml` for the keep-warm ping.
35
 
36
  ## Deploy (drag & drop)
37
 
 
72
  Change `LLM_REPO` / `LLM_FILE` in the `Dockerfile` (confirm exact filenames on
73
  the repo's *Files* tab):
74
 
75
+ - Current (small MoE, fast on CPU): `unsloth/Qwen3.5-0.8B-GGUF` → `Qwen3.5-0.8B-Q4_K_M.gguf`
76
+ - Fastest tiny dense: `Qwen/Qwen2.5-Coder-0.5B-Instruct-GGUF`
77
+ - Best general quality (~½ speed): `HuggingFaceTB/SmolLM2-1.7B-Instruct-GGUF`
78
+
79
+ ### MTP (Multi-Token Prediction) — later
80
+
81
+ `unsloth/Qwen3.5-0.8B-MTP-GGUF` adds self-speculative decoding for a ~1.4–1.7×
82
+ decode speedup. It's **not** a drop-in here: the MTP flags (`--spec-type
83
+ draft-mtp`) live in **`llama-server`**, not the `llama_cpp.Llama()` API this app
84
+ uses. To try it, run `llama-server` as the backend and proxy to it. Feasible on
85
+ CPU only because the model is tiny (an MTP 9B on 2 vCPU stays ~1.5–2.5 tok/s).
86
 
87
  ## Keep it warm (avoid cold starts)
88
 
app.py CHANGED
@@ -1,10 +1,11 @@
1
  """
2
- CPU RAG Space — bge-small (fastembed) + FAISS + Qwen2.5-Coder-0.5B (llama.cpp),
3
  served as an OpenAI-compatible API with a small web UI.
4
 
5
  Everything runs on CPU and fits the Hugging Face free tier (2 vCPU / 16 GB).
6
- Tuned for lowest CPU latency: code-tuned 0.5B model, 2K context, flash-attention,
7
- short answers, and a startup warm-up so the first real query is snappy.
 
8
  """
9
 
10
  import glob
@@ -23,7 +24,7 @@ from pydantic import BaseModel
23
  # Config (all overridable via Space "Variables")
24
  # --------------------------------------------------------------------------- #
25
  MODEL_DIR = os.environ.get("MODEL_DIR", "models")
26
- LLM_FILE = os.environ.get("LLM_FILE", "qwen2.5-coder-0.5b-instruct-q4_k_m.gguf")
27
  LLM_PATH = os.path.join(MODEL_DIR, LLM_FILE)
28
  EMBED_MODEL = os.environ.get("EMBED_MODEL", "BAAI/bge-small-en-v1.5")
29
  FASTEMBED_CACHE = os.environ.get("FASTEMBED_CACHE")
@@ -33,14 +34,23 @@ N_CTX = int(os.environ.get("N_CTX", "2048"))
33
  N_THREADS = int(os.environ.get("N_THREADS", str(os.cpu_count() or 2)))
34
  TOP_K = int(os.environ.get("TOP_K", "4"))
35
  DOCS_DIR = os.environ.get("DOCS_DIR", "documents")
 
 
 
 
36
 
37
  CHUNK_SIZE = 800 # characters per chunk
38
  CHUNK_OVERLAP = 120
39
 
 
 
 
 
 
40
  RAG_SYSTEM = (
41
- "You are a helpful assistant. Answer the user's question using ONLY the "
42
- "context below. If the answer is not in the context, say you don't know. "
43
- "Cite the source of each fact in square brackets like [filename].\n\n"
44
  "Context:\n{context}"
45
  )
46
 
@@ -178,12 +188,20 @@ def _augment(req: ChatRequest):
178
  msgs = [m.model_dump() for m in req.messages]
179
  users = [m for m in msgs if m["role"] == "user"]
180
  query = users[-1]["content"] if users else ""
181
- ctxs = retrieve(query) if req.use_rag else []
 
 
 
 
 
182
  if ctxs:
183
  context = "\n\n".join(f"[{c['source']}] {c['text']}" for c in ctxs)
184
  system = {"role": "system", "content": RAG_SYSTEM.format(context=context)}
185
- msgs = [system] + [m for m in msgs if m["role"] != "system"]
186
- return msgs, ctxs
 
 
 
187
 
188
 
189
  @app.post("/v1/chat/completions")
 
1
  """
2
+ CPU RAG Space — bge-small (fastembed) + FAISS + Qwen3.5-0.8B (llama.cpp),
3
  served as an OpenAI-compatible API with a small web UI.
4
 
5
  Everything runs on CPU and fits the Hugging Face free tier (2 vCPU / 16 GB).
6
+ Tuned for lowest CPU latency: small MoE model, 2K context, flash-attention,
7
+ short answers, relevance-gated RAG, and a startup warm-up so the first real
8
+ query is snappy. Needs llama-cpp-python >= 0.3.32 (qwen35 arch).
9
  """
10
 
11
  import glob
 
24
  # Config (all overridable via Space "Variables")
25
  # --------------------------------------------------------------------------- #
26
  MODEL_DIR = os.environ.get("MODEL_DIR", "models")
27
+ LLM_FILE = os.environ.get("LLM_FILE", "Qwen3.5-0.8B-Q4_K_M.gguf")
28
  LLM_PATH = os.path.join(MODEL_DIR, LLM_FILE)
29
  EMBED_MODEL = os.environ.get("EMBED_MODEL", "BAAI/bge-small-en-v1.5")
30
  FASTEMBED_CACHE = os.environ.get("FASTEMBED_CACHE")
 
34
  N_THREADS = int(os.environ.get("N_THREADS", str(os.cpu_count() or 2)))
35
  TOP_K = int(os.environ.get("TOP_K", "4"))
36
  DOCS_DIR = os.environ.get("DOCS_DIR", "documents")
37
+ # Only inject retrieved context when the best hit is actually relevant (cosine
38
+ # similarity). Below this, treat it as normal chat instead of forcing doc QA —
39
+ # stops greetings/off-topic messages from dredging up irrelevant chunks.
40
+ RAG_MIN_SCORE = float(os.environ.get("RAG_MIN_SCORE", "0.4"))
41
 
42
  CHUNK_SIZE = 800 # characters per chunk
43
  CHUNK_OVERLAP = 120
44
 
45
+ # Plain assistant persona used when nothing relevant is retrieved.
46
+ CHAT_SYSTEM = "You are a friendly, concise assistant."
47
+
48
+ # Used when we DO have relevant context. No literal "[filename]" example — a
49
+ # tiny model will just echo it. Sources are returned separately in the JSON.
50
  RAG_SYSTEM = (
51
+ "You are a helpful assistant. Use the context below to answer the user's "
52
+ "question. If the context does not contain the answer, say so briefly and "
53
+ "answer from your own knowledge if you can. Keep answers concise.\n\n"
54
  "Context:\n{context}"
55
  )
56
 
 
188
  msgs = [m.model_dump() for m in req.messages]
189
  users = [m for m in msgs if m["role"] == "user"]
190
  query = users[-1]["content"] if users else ""
191
+
192
+ # Retrieve, then keep only chunks that clear the relevance bar.
193
+ hits = retrieve(query) if req.use_rag else []
194
+ ctxs = [c for c in hits if c["score"] >= RAG_MIN_SCORE]
195
+
196
+ non_system = [m for m in msgs if m["role"] != "system"]
197
  if ctxs:
198
  context = "\n\n".join(f"[{c['source']}] {c['text']}" for c in ctxs)
199
  system = {"role": "system", "content": RAG_SYSTEM.format(context=context)}
200
+ else:
201
+ # Nothing relevant -> behave like a normal chat assistant, no forced
202
+ # "answer only from context" (which made the model spit citations).
203
+ system = {"role": "system", "content": CHAT_SYSTEM}
204
+ return [system] + non_system, ctxs
205
 
206
 
207
  @app.post("/v1/chat/completions")
documents/sample.txt CHANGED
@@ -7,8 +7,9 @@ Architecture:
7
  text into 384-dimensional vectors and needs no GPU or PyTorch.
8
  - Vector store: FAISS (IndexFlatIP) holds the document vectors in memory and
9
  returns the most similar chunks for a query using cosine similarity.
10
- - Language model: Qwen2.5-1.5B-Instruct in GGUF Q4_K_M form, served by
11
- llama.cpp. It reads the retrieved chunks and writes a grounded answer.
 
12
 
13
  How retrieval works:
14
  1. Your question is embedded into a vector.
@@ -19,7 +20,8 @@ How retrieval works:
19
  Why a small model is fine here:
20
  RAG moves knowledge out of the model's weights and into the retriever, so the
21
  model only needs to read and summarise the provided context rather than
22
- memorise facts. That makes a fast 1.5B model a good fit for CPU serving.
 
23
 
24
  Replace this file with your own .txt or .md documents, or upload files at
25
  runtime through the web UI, and the Space will answer questions about them.
 
7
  text into 384-dimensional vectors and needs no GPU or PyTorch.
8
  - Vector store: FAISS (IndexFlatIP) holds the document vectors in memory and
9
  returns the most similar chunks for a query using cosine similarity.
10
+ - Language model: Qwen3.5-0.8B (a small Mixture-of-Experts model) in GGUF
11
+ Q4_K_M form, served by llama.cpp. It reads the retrieved chunks and writes a
12
+ grounded answer.
13
 
14
  How retrieval works:
15
  1. Your question is embedded into a vector.
 
20
  Why a small model is fine here:
21
  RAG moves knowledge out of the model's weights and into the retriever, so the
22
  model only needs to read and summarise the provided context rather than
23
+ memorise facts. That makes a fast, small model like Qwen3.5-0.8B a good fit for
24
+ CPU serving.
25
 
26
  Replace this file with your own .txt or .md documents, or upload files at
27
  runtime through the web UI, and the Space will answer questions about them.
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>CPU RAG · Qwen2.5-Coder-0.5B + FAISS</title>
7
  <style>
8
  :root { color-scheme: light dark; --bg:#0f1220; --panel:#1a1e33; --acc:#7c8cff; --mut:#8b90a8; }
9
  * { box-sizing: border-box; }
@@ -28,7 +28,7 @@
28
  <body>
29
  <header>
30
  <h1>🦅 CPU RAG</h1>
31
- <span class="tag">Qwen2.5-Coder-0.5B · bge-small · FAISS</span>
32
  <span class="tag" id="stat">…</span>
33
  </header>
34
  <main>
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>CPU RAG · Qwen3.5-0.8B + FAISS</title>
7
  <style>
8
  :root { color-scheme: light dark; --bg:#0f1220; --panel:#1a1e33; --acc:#7c8cff; --mut:#8b90a8; }
9
  * { box-sizing: border-box; }
 
28
  <body>
29
  <header>
30
  <h1>🦅 CPU RAG</h1>
31
+ <span class="tag">Qwen3.5-0.8B · bge-small · FAISS</span>
32
  <span class="tag" id="stat">…</span>
33
  </header>
34
  <main>
requirements.txt CHANGED
@@ -1,8 +1,11 @@
1
- # Pinned to 0.3.2 and built FROM SOURCE (see Dockerfile --no-binary):
2
- # * the prebuilt 0.3.2 wheel is musl-linked -> fails on Debian glibc, and
3
- # * newer wheels (0.3.3x) hit the llama.cpp CPU "set_rows" decode crash.
4
- # Compiling 0.3.2 on this glibc image is the only combo that avoids both.
5
- llama-cpp-python==0.3.2
 
 
 
6
 
7
  # Lightweight CPU embeddings via ONNX (no PyTorch -> small image, fast).
8
  fastembed
 
1
+ # Bumped 0.3.2 -> 0.3.32 (Jun 2026) and still built FROM SOURCE (Dockerfile
2
+ # --no-binary):
3
+ # * Qwen3.5 uses the `qwen35` architecture, which needs llama.cpp >= b9616.
4
+ # The old 0.3.2 pin can't load it ("unknown architecture 'qwen35'").
5
+ # * 0.3.32 vendors a recent llama.cpp (past the old CPU "set_rows" decode
6
+ # crash) and works on Debian glibc when compiled here.
7
+ # If a future wheel regresses, pin the last-known-good 0.3.x that loads qwen35.
8
+ llama-cpp-python==0.3.32
9
 
10
  # Lightweight CPU embeddings via ONNX (no PyTorch -> small image, fast).
11
  fastembed