Instructions to use Neohosseinism/gemma4-stack with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use Neohosseinism/gemma4-stack with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf Neohosseinism/gemma4-stack:Q4_K_M # Run inference directly in the terminal: llama cli -hf Neohosseinism/gemma4-stack:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf Neohosseinism/gemma4-stack:Q4_K_M # Run inference directly in the terminal: llama cli -hf Neohosseinism/gemma4-stack:Q4_K_M
Build from source code
git clone https://github.com/ggml-org/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-app # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama serve -hf Neohosseinism/gemma4-stack:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama cli -hf Neohosseinism/gemma4-stack:Q4_K_M
Use Docker
docker model run hf.co/Neohosseinism/gemma4-stack:Q4_K_M
- Llama new
- LM Studio
- Jan
- Ollama
How to use Neohosseinism/gemma4-stack with Ollama:
ollama run hf.co/Neohosseinism/gemma4-stack:Q4_K_M
- Unsloth Desktop
- Pi
How to use Neohosseinism/gemma4-stack with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Neohosseinism/gemma4-stack:Q4_K_M
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "Neohosseinism/gemma4-stack:Q4_K_M" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use Neohosseinism/gemma4-stack with Docker Model Runner:
docker model run hf.co/Neohosseinism/gemma4-stack:Q4_K_M
- Lemonade
How to use Neohosseinism/gemma4-stack with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull Neohosseinism/gemma4-stack:Q4_K_M
Run and chat with the model
lemonade run user.gemma4-stack-Q4_K_M
List all available models
lemonade list
- Hermes Agent
How to use Neohosseinism/gemma4-stack with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Neohosseinism/gemma4-stack:Q4_K_M
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default Neohosseinism/gemma4-stack:Q4_K_M
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use Neohosseinism/gemma4-stack with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Neohosseinism/gemma4-stack:Q4_K_M
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "Neohosseinism/gemma4-stack:Q4_K_M" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Commit ·
89bf59d
0
Parent(s):
Add gemma4-stack config, scripts, and docker-compose setup
Browse files- .env.example +37 -0
- .gitattributes +6 -0
- .gitignore +18 -0
- Makefile +40 -0
- README.md +130 -0
- assets/test_audio_en.mp3 +3 -0
- assets/test_audio_en.wav +3 -0
- assets/test_image.png +3 -0
- docker-compose.gpu.yml +42 -0
- docker-compose.yml +148 -0
- llama-swap/config.tmpl.yaml +53 -0
- llama-swap/entrypoint.sh +28 -0
- profiles/cpu-server-xl.env +24 -0
- profiles/cpu-server.env +24 -0
- profiles/dev-cpu.env +24 -0
- profiles/gpu.env +29 -0
- scripts/bench.sh +32 -0
- scripts/download-models.sh +52 -0
- scripts/e2e-test.sh +75 -0
.env.example
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# Gemma 4 stack configuration. Copy a profile over this file, e.g.:
|
| 3 |
+
# cp profiles/dev-cpu.env .env
|
| 4 |
+
# then edit HF_TOKEN. Every value below has a sensible default in compose.
|
| 5 |
+
# =============================================================================
|
| 6 |
+
|
| 7 |
+
# --- Hugging Face (required: Gemma 4 is gated) -------------------------------
|
| 8 |
+
HF_TOKEN=
|
| 9 |
+
|
| 10 |
+
# --- Hardware / inference knobs (rendered into llama-swap config) ------------
|
| 11 |
+
NGL=0 # GPU layers to offload. 0 = pure CPU. 99 = all (GPU).
|
| 12 |
+
THREADS=4 # physical cores (this box = 4). Not hyperthreads.
|
| 13 |
+
CTX=8192 # context window per model
|
| 14 |
+
|
| 15 |
+
# --- Default model shown in Open WebUI (must match a llama-swap config key) --
|
| 16 |
+
DEFAULT_MODEL=gemma-e4b # gemma-e4b | gemma-12b | gemma-26b-a4b
|
| 17 |
+
|
| 18 |
+
# --- RAG models (Persian-tuned) ----------------------------------------------
|
| 19 |
+
TEI_EMBED_MODEL=BAAI/bge-m3 # Persian, FaMTEB top-2 (Hakim's HF repo is unpublished)
|
| 20 |
+
TEI_EMBED_POOLING=cls # bge-m3 uses cls pooling
|
| 21 |
+
TEI_RERANK_MODEL=BAAI/bge-reranker-v2-m3
|
| 22 |
+
TEI_DTYPE=float16 # float16 -> lighter Candle backend on CPU (saves RAM)
|
| 23 |
+
|
| 24 |
+
# --- Images (override for GPU via docker-compose.gpu.yml) --------------------
|
| 25 |
+
LLAMASWAP_IMAGE=ghcr.io/mostlygeek/llama-swap:cpu
|
| 26 |
+
TEI_IMAGE=ghcr.io/huggingface/text-embeddings-inference:cpu-1.9
|
| 27 |
+
|
| 28 |
+
# --- Host ports ---------------------------------------------------------------
|
| 29 |
+
WEBUI_PORT=3000
|
| 30 |
+
LLAMASWAP_PORT=8080
|
| 31 |
+
TEI_EMBED_PORT=8081
|
| 32 |
+
TEI_RERANK_PORT=8082
|
| 33 |
+
|
| 34 |
+
# --- Misc ---------------------------------------------------------------------
|
| 35 |
+
OPENAI_API_KEY=sk-local
|
| 36 |
+
TEI_API_KEY=x
|
| 37 |
+
WEBUI_NAME=Gemma 4 Local
|
.gitattributes
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
models/tei/**/blobs/* filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.gguf filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.wav filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# local config & secrets
|
| 2 |
+
.env
|
| 3 |
+
|
| 4 |
+
# rendered llama-swap config
|
| 5 |
+
llama-swap/config.yaml
|
| 6 |
+
|
| 7 |
+
# Open WebUI persistent data (db, vector store, uploads)
|
| 8 |
+
openwebui/data/
|
| 9 |
+
|
| 10 |
+
# bench output
|
| 11 |
+
scripts/bench-results/
|
| 12 |
+
|
| 13 |
+
# transient hf_hub download locks (0-byte)
|
| 14 |
+
*.lock
|
| 15 |
+
|
| 16 |
+
# python
|
| 17 |
+
__pycache__/
|
| 18 |
+
*.pyc
|
Makefile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Minimal helpers for the Gemma 4 stack. Run `make help`.
|
| 2 |
+
ENV ?= dev-cpu
|
| 3 |
+
ARGS ?=
|
| 4 |
+
COMPOSE = docker compose
|
| 5 |
+
|
| 6 |
+
.PHONY: help env models pull up up-gpu down ps logs test clean
|
| 7 |
+
|
| 8 |
+
help: ## Show this help
|
| 9 |
+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
| 10 |
+
awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-10s\033[0m %s\n",$$1,$$2}'
|
| 11 |
+
|
| 12 |
+
env: ## Copy profiles/$(ENV).env -> .env (ENV=dev-cpu|cpu-server|cpu-server-xl|gpu)
|
| 13 |
+
cp profiles/$(ENV).env .env
|
| 14 |
+
|
| 15 |
+
models: ## Download required BF16 mmproj (use ARGS=--weights to also fetch GGUFs)
|
| 16 |
+
./scripts/download-models.sh $(ARGS)
|
| 17 |
+
|
| 18 |
+
pull: ## Pre-pull docker images
|
| 19 |
+
$(COMPOSE) pull
|
| 20 |
+
|
| 21 |
+
up: ## Start the CPU stack
|
| 22 |
+
$(COMPOSE) up -d
|
| 23 |
+
|
| 24 |
+
up-gpu: ## Start the GPU stack (needs profiles/gpu.env)
|
| 25 |
+
$(COMPOSE) -f docker-compose.yml -f docker-compose.gpu.yml up -d
|
| 26 |
+
|
| 27 |
+
down: ## Stop the stack
|
| 28 |
+
$(COMPOSE) down
|
| 29 |
+
|
| 30 |
+
ps: ## Show service status
|
| 31 |
+
$(COMPOSE) ps
|
| 32 |
+
|
| 33 |
+
logs: ## Tail logs (SVC=llama-swap to filter)
|
| 34 |
+
$(COMPOSE) logs -f --tail=120 $(SVC)
|
| 35 |
+
|
| 36 |
+
test: ## Run the e2e smoke tests
|
| 37 |
+
./scripts/e2e-test.sh
|
| 38 |
+
|
| 39 |
+
clean: ## Stop and remove containers + volumes
|
| 40 |
+
$(COMPOSE) down -v
|
README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gemma 4 — local, CPU-first LLM stack (llama.cpp + llama-swap + TEI + Open WebUI)
|
| 2 |
+
|
| 3 |
+
A fully local stack: **Gemma 4** (text + **image** + **native audio**) served by **llama.cpp**
|
| 4 |
+
behind **llama-swap**, with **Persian-tuned RAG** (HF **TEI**: `bge-m3` embedder — top-2 on
|
| 5 |
+
the Persian FaMTEB benchmark — + `bge-reranker-v2-m3`) and **Open WebUI** as the front-end.
|
| 6 |
+
Profiled for CPU servers, with a GPU override.
|
| 7 |
+
|
| 8 |
+
> Embedder note: the Persian-SOTA **Hakim** (`MCINext/Hakim`) has no weights published in its
|
| 9 |
+
> HF repo yet (only a README), so the stack defaults to **`bge-m3`**. Swap `TEI_EMBED_MODEL`
|
| 10 |
+
> back to Hakim once its weights land.
|
| 11 |
+
|
| 12 |
+
```
|
| 13 |
+
Open WebUI ──┬─► llama-swap ─► llama-server (Gemma 4 omni + BF16 mmproj) chat · vision · audio
|
| 14 |
+
├─► TEI-embed (BAAI/bge-m3 · Persian) RAG embeddings
|
| 15 |
+
└─► TEI-rerank (BAAI/bge-reranker-v2-m3) RAG rerank
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
## What runs where
|
| 19 |
+
|
| 20 |
+
| Service | Image (CPU) | Internal URL | Host port |
|
| 21 |
+
|---|---|---|---|
|
| 22 |
+
| Open WebUI | `ghcr.io/open-webui/open-webui:main` | — | `3000` |
|
| 23 |
+
| llama-swap | `ghcr.io/mostlygeek/llama-swap:cpu` | `http://llama-swap:8080/v1` | `8080` |
|
| 24 |
+
| TEI embed | `…/text-embeddings-inference:cpu-1.9` | `http://tei-embed:80` | `8081` |
|
| 25 |
+
| TEI rerank | `…/text-embeddings-inference:cpu-1.9` | `http://tei-rerank:80` | `8082` |
|
| 26 |
+
|
| 27 |
+
## Prerequisites
|
| 28 |
+
- Docker + `docker compose` v2 (`docker compose version`).
|
| 29 |
+
- A Hugging Face token with the **Gemma 4 license accepted** (the model is gated):
|
| 30 |
+
```bash
|
| 31 |
+
pip install -U "huggingface_hub[cli]" && huggingface-cli login
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## Quick start (this CPU dev box)
|
| 35 |
+
```bash
|
| 36 |
+
cd gemma4-stack
|
| 37 |
+
cp profiles/dev-cpu.env .env # E4B default; edit HF_TOKEN=
|
| 38 |
+
./scripts/download-models.sh # pulls the REQUIRED BF16 mmproj files
|
| 39 |
+
docker compose up -d # boots TEI + Open WebUI + llama-swap
|
| 40 |
+
```
|
| 41 |
+
Open <http://localhost:3000>, create the first (admin) account.
|
| 42 |
+
|
| 43 |
+
**One-time in Admin UI:**
|
| 44 |
+
1. **Settings → Models →** enable **Vision** on `gemma-e4b` (lets Open WebUI send images).
|
| 45 |
+
2. **Admin → Functions → +** → paste `openwebui/functions/gemma4_audio_pipe.py` → **Save** → enable.
|
| 46 |
+
- In its valves, confirm `MODEL=gemma-e4b` and `LLAMASWAP_URL=http://llama-swap:8080/v1`.
|
| 47 |
+
3. **Settings → Documents →** confirm embedding engine = **OpenAI**, model = your `TEI_EMBED_MODEL`,
|
| 48 |
+
**Hybrid Search ON**, and the external reranker URL is reachable.
|
| 49 |
+
|
| 50 |
+
## Use it
|
| 51 |
+
- **Text/Persian chat** — pick `gemma-e4b`, chat.
|
| 52 |
+
- **Image** — attach a picture, ask about it (Vision toggle must be on).
|
| 53 |
+
- **Native audio** — pick **“Gemma 4 · Omni (audio)”**, attach a short clip (≤ ~30 s),
|
| 54 |
+
ask your question. Gemma *hears* it (no Whisper).
|
| 55 |
+
- **RAG** — upload Persian docs to a Knowledge / `#`-reference them; retrieval → rerank → answer.
|
| 56 |
+
|
| 57 |
+
## Other machines / profiles
|
| 58 |
+
```bash
|
| 59 |
+
cp profiles/cpu-server.env .env # 12B default; set THREADS=<physical cores>
|
| 60 |
+
docker compose up -d
|
| 61 |
+
|
| 62 |
+
cp profiles/gpu.env .env # GPU box
|
| 63 |
+
docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d
|
| 64 |
+
```
|
| 65 |
+
Switch the live model anytime from the model dropdown — llama-swap loads it on demand and
|
| 66 |
+
unloads the idle one (so RAM never doubles). Profiles: `gemma-e4b`, `gemma-12b`,
|
| 67 |
+
`gemma-26b-a4b` (image-only).
|
| 68 |
+
|
| 69 |
+
## Test it yourself
|
| 70 |
+
**One command — runs all 7 checks (text · image · native audio · embeddings · rerank · UI) using the
|
| 71 |
+
bundled demo assets, no setup:**
|
| 72 |
+
```bash
|
| 73 |
+
make test # expects: RESULT: 7 passed, 0 failed
|
| 74 |
+
```
|
| 75 |
+
In the **browser** (port 3000): pick `gemma-e4b` and chat in Persian; attach `assets/test_image.png`
|
| 76 |
+
and ask about it; for audio, import the Pipe (Admin → Functions) then pick “Gemma 4 · Omni (audio)”
|
| 77 |
+
and attach `assets/test_audio_en.wav`.
|
| 78 |
+
|
| 79 |
+
## Verify from the shell
|
| 80 |
+
```bash
|
| 81 |
+
curl localhost:8080/v1/models # llama-swap lists the profiles
|
| 82 |
+
curl localhost:8081/v1/embeddings -d '{"model":"x","input":["سلام"]}' # embedder
|
| 83 |
+
curl localhost:8082/rerank -d '{"query":"پایتخت ایران","texts":["تهران","موز"]}' # reranker
|
| 84 |
+
# native audio (after: ffmpeg -i clip.any -ar 16000 -ac 1 clip.wav):
|
| 85 |
+
B64=$(base64 -w0 clip.wav); curl localhost:8080/v1/chat/completions -H 'Content-Type: application/json' \
|
| 86 |
+
-d '{"model":"gemma-e4b","messages":[{"role":"user","content":[{"type":"text","text":"این صدا را بنویس"},{"type":"input_audio","input_audio":{"data":"'"$B64"'","format":"wav"}}]}]}'
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## Tuning (`scripts/bench.sh`)
|
| 90 |
+
`./scripts/download-models.sh --weights` then `./scripts/bench.sh /models/gemma-4-E4B-it-Q4_K_M.gguf`
|
| 91 |
+
to sweep `--threads`. Set the winner as `THREADS` in `.env`. Long context? add KV quant
|
| 92 |
+
(`-ctk q8_0 -ctv q8_0`) to the macro in `llama-swap/config.tmpl.yaml`.
|
| 93 |
+
|
| 94 |
+
## Troubleshooting (the known traps)
|
| 95 |
+
- **Audio crashes / asserts** → keep `-b 2048 -ub 2048` (already in the macro). Default 512 crashes.
|
| 96 |
+
- **Audio is garbage/repetitive** → the mmproj must be **BF16** (we pin it). Q8_0/F16 break audio.
|
| 97 |
+
- **`llama-server` not found in llama-swap** → adjust the binary path in `config.tmpl.yaml` / `entrypoint.sh`.
|
| 98 |
+
- **`llama-swap` flags differ** → check `docker run --rm <llamaswap-image> llama-swap --help`; fix `entrypoint.sh`.
|
| 99 |
+
- **Audio “No audio found” in the Pipe** → Open WebUI's file-store API differs by version;
|
| 100 |
+
adapt `_resolve_local_path()` in the Pipe.
|
| 101 |
+
- **Embeddings fail / wrong model** → set every `RAG_*` env explicitly (Open WebUI doesn't inherit
|
| 102 |
+
`OPENAI_*`); verify in Admin → Documents. Hakim won't load? set `TEI_EMBED_MODEL=BAAI/bge-m3`
|
| 103 |
+
and `TEI_EMBED_POOLING=cls`.
|
| 104 |
+
- **Reranker freezes UI / format mismatch** → fall back to Open WebUI's built-in reranker: remove the
|
| 105 |
+
`RAG_RERANKING_ENGINE=external` + URL envs and set `RAG_RERANKING_MODEL=BAAI/bge-reranker-v2-m3`.
|
| 106 |
+
|
| 107 |
+
## Operational notes (validated live on a 15 GB CPU box)
|
| 108 |
+
- **Gemma 4 is a reasoning model** — it emits `reasoning_content` before `content`. Give it a
|
| 109 |
+
generous `max_tokens` (≥256) or short prompts will return empty `content` (all budget spent
|
| 110 |
+
thinking). The Pipe/UI handle this; raw API callers should read both fields.
|
| 111 |
+
- **Send media via a file, not inline** — base64 audio/large images exceed the shell arg limit
|
| 112 |
+
(`argument list too long`). Use `curl -d @payload.json` (the e2e script and Pipe already do).
|
| 113 |
+
- **TEI memory on CPU**: the ONNX backend is heavy (bge-m3 ≈ 4.75 GB). Setting `TEI_DTYPE=float16`
|
| 114 |
+
makes ONNX bow out and TEI falls back to the lighter **Candle** backend (≈ 3.4 GB) — ~2.4 GB
|
| 115 |
+
saved, same 1024-dim Persian output. That's why `--dtype float16` is in the compose commands.
|
| 116 |
+
- **Tight-RAM tuning (this box)**: dev profile uses `CTX=4096`; E4B is loaded by local path
|
| 117 |
+
(`-m /models/...gguf`) so a studio pause/resume doesn't re-download 5 GB. E4B + bge-m3 +
|
| 118 |
+
reranker + UI ≈ 13–14 GB — fits 15 GB but with little headroom; **16–24 GB is comfortable**.
|
| 119 |
+
- **Verified end-to-end here**: Persian text ✓, image→“Red” ✓, native audio→exact transcript ✓,
|
| 120 |
+
Persian embeddings (1024-d) ✓, Persian rerank ordering ✓, Open WebUI ✓.
|
| 121 |
+
|
| 122 |
+
## Layout
|
| 123 |
+
```
|
| 124 |
+
docker-compose.yml · docker-compose.gpu.yml · .env.example
|
| 125 |
+
profiles/{dev-cpu,cpu-server,cpu-server-xl,gpu}.env
|
| 126 |
+
llama-swap/{config.tmpl.yaml,entrypoint.sh}
|
| 127 |
+
openwebui/functions/gemma4_audio_pipe.py
|
| 128 |
+
scripts/{download-models.sh,bench.sh}
|
| 129 |
+
```
|
| 130 |
+
Full design rationale: `../.claude/plans/i-want-make-a-ancient-teapot.md`.
|
assets/test_audio_en.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:92cdf7c1bb8821f84413a9f619e24d76fc26473e56b2b3f2fa2d674b1c4c4dd1
|
| 3 |
+
size 27840
|
assets/test_audio_en.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:26093ab539a21e7bee0df70dcb9e5b107fbeb2cfb796542b0f3f6ade664f76f3
|
| 3 |
+
size 111438
|
assets/test_image.png
ADDED
|
Git LFS Details
|
docker-compose.gpu.yml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# GPU override — layer on top of the base file:
|
| 3 |
+
# docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d
|
| 4 |
+
#
|
| 5 |
+
# Swaps images to CUDA variants, offloads layers to the GPU, and reserves the
|
| 6 |
+
# GPU for the containers. Use with profiles/gpu.env (sets NGL, CTX, models).
|
| 7 |
+
#
|
| 8 |
+
# NOTE: keep --no-mmproj-offload in the llama-swap macro so the BF16 projector
|
| 9 |
+
# stays on CPU and spares VRAM for the model weights (weak-GPU friendly).
|
| 10 |
+
# =============================================================================
|
| 11 |
+
|
| 12 |
+
services:
|
| 13 |
+
llama-swap:
|
| 14 |
+
image: ${LLAMASWAP_IMAGE:-ghcr.io/mostlygeek/llama-swap:cuda}
|
| 15 |
+
deploy:
|
| 16 |
+
resources:
|
| 17 |
+
reservations:
|
| 18 |
+
devices:
|
| 19 |
+
- driver: nvidia
|
| 20 |
+
count: all
|
| 21 |
+
capabilities: [gpu]
|
| 22 |
+
|
| 23 |
+
tei-embed:
|
| 24 |
+
# Default CUDA image targets Turing+; pick an arch-specific tag if needed.
|
| 25 |
+
image: ${TEI_IMAGE:-ghcr.io/huggingface/text-embeddings-inference:1.9}
|
| 26 |
+
deploy:
|
| 27 |
+
resources:
|
| 28 |
+
reservations:
|
| 29 |
+
devices:
|
| 30 |
+
- driver: nvidia
|
| 31 |
+
count: all
|
| 32 |
+
capabilities: [gpu]
|
| 33 |
+
|
| 34 |
+
tei-rerank:
|
| 35 |
+
image: ${TEI_IMAGE:-ghcr.io/huggingface/text-embeddings-inference:1.9}
|
| 36 |
+
deploy:
|
| 37 |
+
resources:
|
| 38 |
+
reservations:
|
| 39 |
+
devices:
|
| 40 |
+
- driver: nvidia
|
| 41 |
+
count: all
|
| 42 |
+
capabilities: [gpu]
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# Gemma 4 CPU-first stack — base (CPU) compose file.
|
| 3 |
+
#
|
| 4 |
+
# Services:
|
| 5 |
+
# llama-swap : OpenAI endpoint that hot-swaps Gemma 4 GGUF profiles
|
| 6 |
+
# tei-embed : HF Text-Embeddings-Inference — Persian embedder (Hakim/bge-m3)
|
| 7 |
+
# tei-rerank : HF Text-Embeddings-Inference — reranker (bge-reranker-v2-m3)
|
| 8 |
+
# openwebui : chat UI + RAG orchestrator + native-audio Pipe host
|
| 9 |
+
#
|
| 10 |
+
# Usage (CPU): docker compose up -d
|
| 11 |
+
# Usage (GPU): docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d
|
| 12 |
+
#
|
| 13 |
+
# All tunables come from .env (copy one of profiles/*.env → .env first).
|
| 14 |
+
# =============================================================================
|
| 15 |
+
|
| 16 |
+
x-common: &common
|
| 17 |
+
restart: unless-stopped
|
| 18 |
+
networks: [stack]
|
| 19 |
+
|
| 20 |
+
services:
|
| 21 |
+
# ---------------------------------------------------------------------------
|
| 22 |
+
# LLM gateway: llama-swap renders its config from .env, then loads Gemma 4
|
| 23 |
+
# llama-server on demand (one model resident at a time, idle-unloaded by ttl).
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
llama-swap:
|
| 26 |
+
<<: *common
|
| 27 |
+
image: ${LLAMASWAP_IMAGE:-ghcr.io/mostlygeek/llama-swap:cpu}
|
| 28 |
+
container_name: llama-swap
|
| 29 |
+
entrypoint: ["/bin/sh", "/config/entrypoint.sh"]
|
| 30 |
+
environment:
|
| 31 |
+
# Rendered into config.yaml by entrypoint.sh (sed), per-machine:
|
| 32 |
+
NGL: ${NGL:-0} # GPU layers to offload (0 = pure CPU)
|
| 33 |
+
THREADS: ${THREADS:-4} # physical cores, not hyperthreads
|
| 34 |
+
CTX: ${CTX:-8192} # context window
|
| 35 |
+
HF_TOKEN: ${HF_TOKEN:-} # for gated Gemma 4 pulls via -hf
|
| 36 |
+
HF_HOME: /models/hf-cache
|
| 37 |
+
LLAMA_CACHE: /models/llama-cache # persist GGUFs fetched by -hf across restarts
|
| 38 |
+
volumes:
|
| 39 |
+
- ./llama-swap:/config
|
| 40 |
+
- ./models:/models
|
| 41 |
+
ports:
|
| 42 |
+
- "${LLAMASWAP_PORT:-8080}:8080"
|
| 43 |
+
healthcheck:
|
| 44 |
+
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/v1/models || exit 1"]
|
| 45 |
+
interval: 30s
|
| 46 |
+
timeout: 5s
|
| 47 |
+
retries: 3
|
| 48 |
+
start_period: 40s
|
| 49 |
+
|
| 50 |
+
# ---------------------------------------------------------------------------
|
| 51 |
+
# RAG embedder (Persian). Default Hakim (FaMTEB #1); fallback bge-m3.
|
| 52 |
+
# Container listens on :80. OpenAI-compatible /v1/embeddings + native /embed.
|
| 53 |
+
# ---------------------------------------------------------------------------
|
| 54 |
+
tei-embed:
|
| 55 |
+
<<: *common
|
| 56 |
+
image: ${TEI_IMAGE:-ghcr.io/huggingface/text-embeddings-inference:cpu-1.9}
|
| 57 |
+
container_name: tei-embed
|
| 58 |
+
command: >-
|
| 59 |
+
--model-id ${TEI_EMBED_MODEL:-BAAI/bge-m3}
|
| 60 |
+
--pooling ${TEI_EMBED_POOLING:-cls}
|
| 61 |
+
--dtype ${TEI_DTYPE:-float16}
|
| 62 |
+
--max-batch-tokens 4096
|
| 63 |
+
--max-client-batch-size 8
|
| 64 |
+
--auto-truncate
|
| 65 |
+
environment:
|
| 66 |
+
HF_TOKEN: ${HF_TOKEN:-}
|
| 67 |
+
volumes:
|
| 68 |
+
- ./models/tei:/data
|
| 69 |
+
ports:
|
| 70 |
+
- "${TEI_EMBED_PORT:-8081}:80"
|
| 71 |
+
healthcheck:
|
| 72 |
+
test: ["CMD-SHELL", "curl -fsS http://localhost:80/health || exit 1"]
|
| 73 |
+
interval: 30s
|
| 74 |
+
timeout: 5s
|
| 75 |
+
retries: 5
|
| 76 |
+
start_period: 60s
|
| 77 |
+
|
| 78 |
+
# ---------------------------------------------------------------------------
|
| 79 |
+
# RAG reranker (cross-encoder). bge-reranker-v2-m3 covers Persian well.
|
| 80 |
+
# Exposes /rerank (Open WebUI external reranker points here).
|
| 81 |
+
# ---------------------------------------------------------------------------
|
| 82 |
+
tei-rerank:
|
| 83 |
+
<<: *common
|
| 84 |
+
image: ${TEI_IMAGE:-ghcr.io/huggingface/text-embeddings-inference:cpu-1.9}
|
| 85 |
+
container_name: tei-rerank
|
| 86 |
+
command: >-
|
| 87 |
+
--model-id ${TEI_RERANK_MODEL:-BAAI/bge-reranker-v2-m3}
|
| 88 |
+
--dtype ${TEI_DTYPE:-float16}
|
| 89 |
+
--max-batch-tokens 4096
|
| 90 |
+
--auto-truncate
|
| 91 |
+
environment:
|
| 92 |
+
HF_TOKEN: ${HF_TOKEN:-}
|
| 93 |
+
volumes:
|
| 94 |
+
- ./models/tei:/data
|
| 95 |
+
ports:
|
| 96 |
+
- "${TEI_RERANK_PORT:-8082}:80"
|
| 97 |
+
healthcheck:
|
| 98 |
+
test: ["CMD-SHELL", "curl -fsS http://localhost:80/health || exit 1"]
|
| 99 |
+
interval: 30s
|
| 100 |
+
timeout: 5s
|
| 101 |
+
retries: 5
|
| 102 |
+
start_period: 60s
|
| 103 |
+
|
| 104 |
+
# ---------------------------------------------------------------------------
|
| 105 |
+
# Front-end + RAG orchestrator. Talks to llama-swap (chat/vision) and the
|
| 106 |
+
# two TEI services (embed/rerank). Native audio handled by the imported Pipe.
|
| 107 |
+
# ---------------------------------------------------------------------------
|
| 108 |
+
openwebui:
|
| 109 |
+
<<: *common
|
| 110 |
+
image: ghcr.io/open-webui/open-webui:main
|
| 111 |
+
container_name: openwebui
|
| 112 |
+
environment:
|
| 113 |
+
# --- chat backend -------------------------------------------------------
|
| 114 |
+
OPENAI_API_BASE_URL: http://llama-swap:8080/v1
|
| 115 |
+
OPENAI_API_KEY: ${OPENAI_API_KEY:-sk-local}
|
| 116 |
+
ENABLE_OLLAMA_API: "false"
|
| 117 |
+
DEFAULT_MODELS: ${DEFAULT_MODEL:-gemma-e4b}
|
| 118 |
+
# --- RAG embeddings via TEI (set RAG_* explicitly; OWUI does not inherit
|
| 119 |
+
# OPENAI_* — see Open WebUI issues #8697 / #22084) ------------------
|
| 120 |
+
RAG_EMBEDDING_ENGINE: openai
|
| 121 |
+
RAG_OPENAI_API_BASE_URL: http://tei-embed:80/v1
|
| 122 |
+
RAG_OPENAI_API_KEY: ${TEI_API_KEY:-x}
|
| 123 |
+
RAG_EMBEDDING_MODEL: ${TEI_EMBED_MODEL:-MCINext/Hakim}
|
| 124 |
+
RAG_EMBEDDING_BATCH_SIZE: "16"
|
| 125 |
+
# --- hybrid retrieval + external reranker via TEI ----------------------
|
| 126 |
+
ENABLE_RAG_HYBRID_SEARCH: "true"
|
| 127 |
+
RAG_RERANKING_ENGINE: external
|
| 128 |
+
RAG_EXTERNAL_RERANKER_URL: http://tei-rerank:80/rerank
|
| 129 |
+
RAG_EXTERNAL_RERANKER_API_KEY: ${TEI_API_KEY:-x}
|
| 130 |
+
RAG_TOP_K: "8"
|
| 131 |
+
RAG_TOP_K_RERANKER: "4"
|
| 132 |
+
# --- native audio is the Pipe's job; turn STT off ----------------------
|
| 133 |
+
AUDIO_STT_ENGINE: ""
|
| 134 |
+
WEBUI_NAME: ${WEBUI_NAME:-Gemma 4 Local}
|
| 135 |
+
volumes:
|
| 136 |
+
- ./openwebui/data:/app/backend/data
|
| 137 |
+
# functions/ is mounted for convenience; import the Pipe via Admin → Functions
|
| 138 |
+
- ./openwebui/functions:/app/backend/data/functions-src:ro
|
| 139 |
+
ports:
|
| 140 |
+
- "${WEBUI_PORT:-3000}:8080"
|
| 141 |
+
depends_on:
|
| 142 |
+
- llama-swap
|
| 143 |
+
- tei-embed
|
| 144 |
+
- tei-rerank
|
| 145 |
+
|
| 146 |
+
networks:
|
| 147 |
+
stack:
|
| 148 |
+
driver: bridge
|
llama-swap/config.tmpl.yaml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# llama-swap config TEMPLATE. entrypoint.sh sed-substitutes ${NGL} ${THREADS}
|
| 3 |
+
# ${CTX} from the environment, then llama-swap resolves ${base} (macro) and
|
| 4 |
+
# ${PORT} (its own dynamic per-model port) at runtime. Do NOT pre-substitute
|
| 5 |
+
# ${base} or ${PORT}.
|
| 6 |
+
#
|
| 7 |
+
# Each "models:" key is the model name you pass in the OpenAI API "model" field
|
| 8 |
+
# (and what Open WebUI shows). ttl unloads an idle model so RAM frees for the
|
| 9 |
+
# next swap.
|
| 10 |
+
#
|
| 11 |
+
# Audio correctness (E4B / 12B): BF16 mmproj + -ub 2048 + --jinja are mandatory.
|
| 12 |
+
# Binary note: the llama-swap "unified" images ship llama-server on PATH. If your
|
| 13 |
+
# image differs, replace "llama-server" below with its absolute path.
|
| 14 |
+
# =============================================================================
|
| 15 |
+
|
| 16 |
+
healthCheckTimeout: 300
|
| 17 |
+
logLevel: info
|
| 18 |
+
|
| 19 |
+
macros:
|
| 20 |
+
base: >-
|
| 21 |
+
llama-server --host 0.0.0.0 --port ${PORT}
|
| 22 |
+
--jinja --no-mmproj-offload -ngl ${NGL}
|
| 23 |
+
--threads ${THREADS} -b 2048 -ub 2048
|
| 24 |
+
--temp 1.0 --top-k 64 --top-p 0.95
|
| 25 |
+
--metrics
|
| 26 |
+
|
| 27 |
+
models:
|
| 28 |
+
# ---- E4B: audio + image + text (dev default, ~7-8 GB) ---------------------
|
| 29 |
+
"gemma-e4b":
|
| 30 |
+
cmd: >-
|
| 31 |
+
${base}
|
| 32 |
+
-m /models/gemma-4-E4B-it-Q4_K_M.gguf
|
| 33 |
+
--mmproj /models/mmproj-gemma-4-E4B-it-bf16.gguf
|
| 34 |
+
-c ${CTX}
|
| 35 |
+
ttl: 600
|
| 36 |
+
|
| 37 |
+
# ---- 12B: audio + image + text (server default, ~10-12 GB) ----------------
|
| 38 |
+
"gemma-12b":
|
| 39 |
+
cmd: >-
|
| 40 |
+
${base}
|
| 41 |
+
-hf ggml-org/gemma-4-12B-it-GGUF:Q4_K_M
|
| 42 |
+
--mmproj /models/mmproj-gemma-4-12B-it-bf16.gguf
|
| 43 |
+
-c ${CTX}
|
| 44 |
+
ttl: 600
|
| 45 |
+
|
| 46 |
+
# ---- 26B-A4B MoE: image + text ONLY, no audio (~16-18 GB) -----------------
|
| 47 |
+
"gemma-26b-a4b":
|
| 48 |
+
cmd: >-
|
| 49 |
+
${base}
|
| 50 |
+
-hf ggml-org/gemma-4-26B-A4B-it-GGUF:Q4_K_M
|
| 51 |
+
--mmproj /models/mmproj-gemma-4-26B-A4B-it-bf16.gguf
|
| 52 |
+
-c ${CTX}
|
| 53 |
+
ttl: 300
|
llama-swap/entrypoint.sh
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
# Render llama-swap config from the template using env knobs, then launch.
|
| 3 |
+
# Substitutes only ${NGL} ${THREADS} ${CTX} — leaves ${base} and ${PORT} for
|
| 4 |
+
# llama-swap to resolve itself.
|
| 5 |
+
set -eu
|
| 6 |
+
|
| 7 |
+
NGL="${NGL:-0}"
|
| 8 |
+
THREADS="${THREADS:-4}"
|
| 9 |
+
CTX="${CTX:-8192}"
|
| 10 |
+
|
| 11 |
+
TMPL="/config/config.tmpl.yaml"
|
| 12 |
+
OUT="/tmp/config.yaml"
|
| 13 |
+
|
| 14 |
+
sed \
|
| 15 |
+
-e "s|\${NGL}|${NGL}|g" \
|
| 16 |
+
-e "s|\${THREADS}|${THREADS}|g" \
|
| 17 |
+
-e "s|\${CTX}|${CTX}|g" \
|
| 18 |
+
"$TMPL" > "$OUT"
|
| 19 |
+
|
| 20 |
+
echo "===================================================================="
|
| 21 |
+
echo "[entrypoint] rendered $OUT (NGL=$NGL THREADS=$THREADS CTX=$CTX)"
|
| 22 |
+
echo "===================================================================="
|
| 23 |
+
cat "$OUT"
|
| 24 |
+
echo "===================================================================="
|
| 25 |
+
|
| 26 |
+
BIN="$(command -v llama-swap || echo /app/llama-swap)"
|
| 27 |
+
echo "[entrypoint] launching: $BIN --config $OUT --listen 0.0.0.0:8080"
|
| 28 |
+
exec "$BIN" --config "$OUT" --listen 0.0.0.0:8080
|
profiles/cpu-server-xl.env
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Profile: large CPU server (>= 24 GiB RAM) for the 26B-A4B MoE.
|
| 2 |
+
# NOTE: 26B-A4B is IMAGE + TEXT only (no audio). Use gemma-12b if you need audio.
|
| 3 |
+
HF_TOKEN=
|
| 4 |
+
|
| 5 |
+
NGL=0
|
| 6 |
+
THREADS=24 # <-- physical cores
|
| 7 |
+
CTX=16384
|
| 8 |
+
DEFAULT_MODEL=gemma-26b-a4b
|
| 9 |
+
|
| 10 |
+
TEI_EMBED_MODEL=BAAI/bge-m3
|
| 11 |
+
TEI_EMBED_POOLING=cls
|
| 12 |
+
TEI_RERANK_MODEL=BAAI/bge-reranker-v2-m3
|
| 13 |
+
TEI_DTYPE=float16 # float16 -> lighter Candle backend on CPU (saves RAM)
|
| 14 |
+
|
| 15 |
+
LLAMASWAP_IMAGE=ghcr.io/mostlygeek/llama-swap:cpu
|
| 16 |
+
TEI_IMAGE=ghcr.io/huggingface/text-embeddings-inference:cpu-1.9
|
| 17 |
+
|
| 18 |
+
WEBUI_PORT=3000
|
| 19 |
+
LLAMASWAP_PORT=8080
|
| 20 |
+
TEI_EMBED_PORT=8081
|
| 21 |
+
TEI_RERANK_PORT=8082
|
| 22 |
+
OPENAI_API_KEY=sk-local
|
| 23 |
+
TEI_API_KEY=x
|
| 24 |
+
WEBUI_NAME=Gemma 4 Local (xl)
|
profiles/cpu-server.env
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Profile: a bigger CPU server (>= 16 GiB RAM, many cores).
|
| 2 |
+
# Default model 12B (audio+image+text). Set THREADS to the server's PHYSICAL cores.
|
| 3 |
+
HF_TOKEN=
|
| 4 |
+
|
| 5 |
+
NGL=0
|
| 6 |
+
THREADS=16 # <-- set to physical core count of the server
|
| 7 |
+
CTX=16384
|
| 8 |
+
DEFAULT_MODEL=gemma-12b
|
| 9 |
+
|
| 10 |
+
TEI_EMBED_MODEL=BAAI/bge-m3
|
| 11 |
+
TEI_EMBED_POOLING=cls
|
| 12 |
+
TEI_RERANK_MODEL=BAAI/bge-reranker-v2-m3
|
| 13 |
+
TEI_DTYPE=float16 # float16 -> lighter Candle backend on CPU (saves RAM)
|
| 14 |
+
|
| 15 |
+
LLAMASWAP_IMAGE=ghcr.io/mostlygeek/llama-swap:cpu
|
| 16 |
+
TEI_IMAGE=ghcr.io/huggingface/text-embeddings-inference:cpu-1.9
|
| 17 |
+
|
| 18 |
+
WEBUI_PORT=3000
|
| 19 |
+
LLAMASWAP_PORT=8080
|
| 20 |
+
TEI_EMBED_PORT=8081
|
| 21 |
+
TEI_RERANK_PORT=8082
|
| 22 |
+
OPENAI_API_KEY=sk-local
|
| 23 |
+
TEI_API_KEY=x
|
| 24 |
+
WEBUI_NAME=Gemma 4 Local (server)
|
profiles/dev-cpu.env
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Profile: this 15 GiB Lightning dev box (Xeon 8470, 4 phys cores, no GPU).
|
| 2 |
+
# Default model E4B (audio+image+text) — comfortably fits with TEI + Open WebUI.
|
| 3 |
+
HF_TOKEN=
|
| 4 |
+
|
| 5 |
+
NGL=0
|
| 6 |
+
THREADS=4
|
| 7 |
+
CTX=8192
|
| 8 |
+
DEFAULT_MODEL=gemma-e4b
|
| 9 |
+
|
| 10 |
+
TEI_EMBED_MODEL=BAAI/bge-m3
|
| 11 |
+
TEI_EMBED_POOLING=cls
|
| 12 |
+
TEI_RERANK_MODEL=BAAI/bge-reranker-v2-m3
|
| 13 |
+
TEI_DTYPE=float16 # float16 -> lighter Candle backend on CPU (saves RAM)
|
| 14 |
+
|
| 15 |
+
LLAMASWAP_IMAGE=ghcr.io/mostlygeek/llama-swap:cpu
|
| 16 |
+
TEI_IMAGE=ghcr.io/huggingface/text-embeddings-inference:cpu-1.9
|
| 17 |
+
|
| 18 |
+
WEBUI_PORT=3000
|
| 19 |
+
LLAMASWAP_PORT=8080
|
| 20 |
+
TEI_EMBED_PORT=8081
|
| 21 |
+
TEI_RERANK_PORT=8082
|
| 22 |
+
OPENAI_API_KEY=sk-local
|
| 23 |
+
TEI_API_KEY=x
|
| 24 |
+
WEBUI_NAME=Gemma 4 Local (dev)
|
profiles/gpu.env
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Profile: a (weak) GPU box. Use with the GPU override:
|
| 2 |
+
# cp profiles/gpu.env .env
|
| 3 |
+
# docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d
|
| 4 |
+
#
|
| 5 |
+
# NGL=99 offloads all model layers; lower it (e.g. 20) if VRAM < model size.
|
| 6 |
+
# The BF16 mmproj stays on CPU (--no-mmproj-offload in the llama-swap macro).
|
| 7 |
+
HF_TOKEN=
|
| 8 |
+
|
| 9 |
+
NGL=99
|
| 10 |
+
THREADS=8
|
| 11 |
+
CTX=32768
|
| 12 |
+
DEFAULT_MODEL=gemma-12b
|
| 13 |
+
|
| 14 |
+
TEI_EMBED_MODEL=BAAI/bge-m3
|
| 15 |
+
TEI_EMBED_POOLING=cls
|
| 16 |
+
TEI_RERANK_MODEL=BAAI/bge-reranker-v2-m3
|
| 17 |
+
TEI_DTYPE=float16 # float16 -> lighter Candle backend on CPU (saves RAM)
|
| 18 |
+
|
| 19 |
+
# CUDA images (override base):
|
| 20 |
+
LLAMASWAP_IMAGE=ghcr.io/mostlygeek/llama-swap:cuda
|
| 21 |
+
TEI_IMAGE=ghcr.io/huggingface/text-embeddings-inference:1.9
|
| 22 |
+
|
| 23 |
+
WEBUI_PORT=3000
|
| 24 |
+
LLAMASWAP_PORT=8080
|
| 25 |
+
TEI_EMBED_PORT=8081
|
| 26 |
+
TEI_RERANK_PORT=8082
|
| 27 |
+
OPENAI_API_KEY=sk-local
|
| 28 |
+
TEI_API_KEY=x
|
| 29 |
+
WEBUI_NAME=Gemma 4 Local (gpu)
|
scripts/bench.sh
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Sweep llama-bench across thread counts on this machine to pick the best
|
| 3 |
+
# --threads for a given Gemma 4 GGUF. Results saved under scripts/bench-results/.
|
| 4 |
+
#
|
| 5 |
+
# ./bench.sh /models/gemma-4-E4B-it-Q4_K_M.gguf
|
| 6 |
+
# THREADS_LIST="2 4 8" ./bench.sh <gguf-path-inside-container>
|
| 7 |
+
#
|
| 8 |
+
# Requires the GGUF present in ./models (pre-download with download-models.sh
|
| 9 |
+
# --weights). Uses the prebuilt llama.cpp image (CPU).
|
| 10 |
+
set -euo pipefail
|
| 11 |
+
cd "$(dirname "$0")/.."
|
| 12 |
+
|
| 13 |
+
MODEL="${1:?usage: bench.sh /models/<file>.gguf (path is inside the container)}"
|
| 14 |
+
THREADS_LIST="${THREADS_LIST:-1 2 4 8}"
|
| 15 |
+
IMAGE="${LLAMA_IMAGE:-ghcr.io/ggml-org/llama.cpp:full}"
|
| 16 |
+
OUT="scripts/bench-results"; mkdir -p "$OUT"
|
| 17 |
+
STAMP="$(date +%Y%m%d-%H%M%S)"
|
| 18 |
+
REPORT="$OUT/bench-$STAMP.md"
|
| 19 |
+
|
| 20 |
+
echo "# llama-bench $(uname -n) $STAMP" | tee "$REPORT"
|
| 21 |
+
echo "model: $MODEL" | tee -a "$REPORT"
|
| 22 |
+
echo | tee -a "$REPORT"
|
| 23 |
+
|
| 24 |
+
for t in $THREADS_LIST; do
|
| 25 |
+
echo "## threads=$t" | tee -a "$REPORT"
|
| 26 |
+
docker run --rm -v "$PWD/models:/models" "$IMAGE" \
|
| 27 |
+
llama-bench -m "$MODEL" -t "$t" -p 512 -n 128 2>/dev/null \
|
| 28 |
+
| tee -a "$REPORT" || echo " (run failed for t=$t)" | tee -a "$REPORT"
|
| 29 |
+
echo | tee -a "$REPORT"
|
| 30 |
+
done
|
| 31 |
+
|
| 32 |
+
echo "Saved: $REPORT"
|
scripts/download-models.sh
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Download the Gemma 4 BF16 mmproj projectors (REQUIRED — must be pinned, never
|
| 3 |
+
# auto-fetched) and, optionally, the LLM GGUF weights. The LLM weights are also
|
| 4 |
+
# fetched automatically by llama-swap's `-hf ...:Q4_K_M` on first request, so by
|
| 5 |
+
# default we only pull the mmproj files.
|
| 6 |
+
#
|
| 7 |
+
# ./download-models.sh # mmproj only (fast, required)
|
| 8 |
+
# ./download-models.sh --weights # also pre-pull the Q4_K_M GGUFs
|
| 9 |
+
#
|
| 10 |
+
# Prereq: huggingface-cli logged in (Gemma is gated):
|
| 11 |
+
# pip install -U "huggingface_hub[cli]" && huggingface-cli login
|
| 12 |
+
# (and accept the Gemma 4 license once on huggingface.co)
|
| 13 |
+
set -euo pipefail
|
| 14 |
+
|
| 15 |
+
cd "$(dirname "$0")/.."
|
| 16 |
+
DEST="models"
|
| 17 |
+
mkdir -p "$DEST"
|
| 18 |
+
|
| 19 |
+
HF="$(command -v huggingface-cli || command -v hf || true)"
|
| 20 |
+
if [ -z "$HF" ]; then
|
| 21 |
+
echo "ERROR: huggingface-cli not found. Run: pip install -U 'huggingface_hub[cli]'" >&2
|
| 22 |
+
exit 1
|
| 23 |
+
fi
|
| 24 |
+
|
| 25 |
+
# repo : mmproj-filename : quant-filename
|
| 26 |
+
MODELS="
|
| 27 |
+
ggml-org/gemma-4-E4B-it-GGUF:mmproj-gemma-4-E4B-it-bf16.gguf:gemma-4-E4B-it-Q4_K_M.gguf
|
| 28 |
+
ggml-org/gemma-4-12B-it-GGUF:mmproj-gemma-4-12B-it-bf16.gguf:gemma-4-12B-it-Q4_K_M.gguf
|
| 29 |
+
ggml-org/gemma-4-26B-A4B-it-GGUF:mmproj-gemma-4-26B-A4B-it-bf16.gguf:gemma-4-26B-A4B-it-Q4_K_M.gguf
|
| 30 |
+
"
|
| 31 |
+
|
| 32 |
+
WEIGHTS=0
|
| 33 |
+
[ "${1:-}" = "--weights" ] && WEIGHTS=1
|
| 34 |
+
|
| 35 |
+
dl() { # repo file
|
| 36 |
+
echo ">> $1 :: $2"
|
| 37 |
+
"$HF" download "$1" "$2" --local-dir "$DEST" || \
|
| 38 |
+
echo " !! could not fetch $2 — verify the exact filename on the HF repo 'Files' tab" >&2
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
for row in $MODELS; do
|
| 42 |
+
repo="${row%%:*}"; rest="${row#*:}"
|
| 43 |
+
mmproj="${rest%%:*}"; quant="${rest#*:}"
|
| 44 |
+
dl "$repo" "$mmproj" # REQUIRED (BF16, pinned for audio)
|
| 45 |
+
[ "$WEIGHTS" = "1" ] && dl "$repo" "$quant"
|
| 46 |
+
done
|
| 47 |
+
|
| 48 |
+
echo
|
| 49 |
+
echo "Done. mmproj files:"
|
| 50 |
+
ls -lh "$DEST"/mmproj-*.gguf 2>/dev/null || echo " (none — check errors above)"
|
| 51 |
+
echo
|
| 52 |
+
echo "TEI embedder/reranker download themselves on first start into models/tei/."
|
scripts/e2e-test.sh
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# End-to-end smoke tests for the Gemma 4 stack. Exit non-zero if anything fails.
|
| 3 |
+
# ./scripts/e2e-test.sh
|
| 4 |
+
# Env overrides: LS=http://localhost:8080 EMB=http://localhost:8081
|
| 5 |
+
# RRK=http://localhost:8082 OWUI=http://localhost:3000
|
| 6 |
+
# MODEL=gemma-e4b TEST_WAV=/path/clip.wav TEST_IMG=/path/img.png
|
| 7 |
+
set -u
|
| 8 |
+
|
| 9 |
+
LS="${LS:-http://localhost:8080}"
|
| 10 |
+
EMB="${EMB:-http://localhost:8081}"
|
| 11 |
+
RRK="${RRK:-http://localhost:8082}"
|
| 12 |
+
OWUI="${OWUI:-http://localhost:3000}"
|
| 13 |
+
MODEL="${MODEL:-gemma-e4b}"
|
| 14 |
+
# Auto-use the bundled demo assets so `make test` needs no setup.
|
| 15 |
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
| 16 |
+
TEST_IMG="${TEST_IMG:-$ROOT/assets/test_image.png}"
|
| 17 |
+
TEST_WAV="${TEST_WAV:-$ROOT/assets/test_audio_en.wav}"
|
| 18 |
+
|
| 19 |
+
PASS=0; FAIL=0
|
| 20 |
+
ok(){ echo " ✅ $1"; PASS=$((PASS+1)); }
|
| 21 |
+
no(){ echo " ❌ $1"; FAIL=$((FAIL+1)); }
|
| 22 |
+
hdr(){ echo; echo "== $1 =="; }
|
| 23 |
+
|
| 24 |
+
jqget(){ python3 -c "import sys,json;d=json.load(sys.stdin);print(eval(sys.argv[1]))" "$1" 2>/dev/null; }
|
| 25 |
+
|
| 26 |
+
hdr "1) llama-swap lists model profiles"
|
| 27 |
+
MODELS=$(curl -fsS "$LS/v1/models" 2>/dev/null)
|
| 28 |
+
echo "$MODELS" | grep -q "$MODEL" && ok "model '$MODEL' present" || no "model '$MODEL' missing"
|
| 29 |
+
|
| 30 |
+
hdr "2) Gemma text chat (Persian)"
|
| 31 |
+
RESP=$(curl -fsS "$LS/v1/chat/completions" -H 'Content-Type: application/json' -d \
|
| 32 |
+
"{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"به فارسی یک جمله کوتاه بنویس.\"}],\"max_tokens\":512,\"temperature\":1.0}" 2>/dev/null)
|
| 33 |
+
TXT=$(echo "$RESP" | jqget "(d['choices'][0]['message'].get('content') or d['choices'][0]['message'].get('reasoning_content') or '')")
|
| 34 |
+
[ -n "$TXT" ] && ok "got reply: ${TXT:0:60}" || no "no text reply ($RESP)"
|
| 35 |
+
|
| 36 |
+
hdr "3) Gemma vision (image)"
|
| 37 |
+
if [ -n "$TEST_IMG" ] && [ -f "$TEST_IMG" ]; then
|
| 38 |
+
python3 -c "import json,base64;b=base64.b64encode(open('$TEST_IMG','rb').read()).decode();json.dump({'model':'$MODEL','messages':[{'role':'user','content':[{'type':'text','text':'Describe this image in one short sentence.'},{'type':'image_url','image_url':{'url':'data:image/png;base64,'+b}}]}],'max_tokens':512},open('/tmp/e2e_img.json','w'))"
|
| 39 |
+
R=$(curl -s -m 600 "$LS/v1/chat/completions" -H 'Content-Type: application/json' -d @/tmp/e2e_img.json 2>/dev/null)
|
| 40 |
+
V=$(echo "$R" | jqget "(d['choices'][0]['message'].get('content') or d['choices'][0]['message'].get('reasoning_content') or '')")
|
| 41 |
+
[ -n "$V" ] && ok "vision reply: ${V:0:60}" || no "no vision reply ($R)"
|
| 42 |
+
else
|
| 43 |
+
echo " ⏭ skipped (set TEST_IMG=/path/img.png)"
|
| 44 |
+
fi
|
| 45 |
+
|
| 46 |
+
hdr "4) Gemma native audio (input_audio)"
|
| 47 |
+
if [ -n "$TEST_WAV" ] && [ -f "$TEST_WAV" ]; then
|
| 48 |
+
python3 -c "import json,base64;b=base64.b64encode(open('$TEST_WAV','rb').read()).decode();json.dump({'model':'$MODEL','messages':[{'role':'user','content':[{'type':'text','text':'Transcribe this audio exactly.'},{'type':'input_audio','input_audio':{'data':b,'format':'wav'}}]}],'max_tokens':512,'temperature':1.0},open('/tmp/e2e_aud.json','w'))"
|
| 49 |
+
R=$(curl -s -m 1500 "$LS/v1/chat/completions" -H 'Content-Type: application/json' -d @/tmp/e2e_aud.json 2>/dev/null)
|
| 50 |
+
A=$(echo "$R" | jqget "(d['choices'][0]['message'].get('content') or d['choices'][0]['message'].get('reasoning_content') or '')")
|
| 51 |
+
[ -n "$A" ] && ok "audio reply: ${A:0:80}" || no "no audio reply ($R)"
|
| 52 |
+
else
|
| 53 |
+
echo " ⏭ skipped (set TEST_WAV=/path/clip.wav)"
|
| 54 |
+
fi
|
| 55 |
+
|
| 56 |
+
hdr "5) TEI embeddings (Persian)"
|
| 57 |
+
R=$(curl -fsS "$EMB/v1/embeddings" -H 'Content-Type: application/json' -d \
|
| 58 |
+
'{"model":"x","input":["پایتخت ایران تهران است"]}' 2>/dev/null)
|
| 59 |
+
DIM=$(echo "$R" | jqget "len(d['data'][0]['embedding'])")
|
| 60 |
+
[ -n "$DIM" ] && [ "$DIM" -gt 0 ] 2>/dev/null && ok "embedding dim=$DIM" || no "no embedding ($R)"
|
| 61 |
+
|
| 62 |
+
hdr "6) TEI rerank (Persian relevance ordering)"
|
| 63 |
+
R=$(curl -fsS "$RRK/rerank" -H 'Content-Type: application/json' -d \
|
| 64 |
+
'{"query":"پایتخت ایران کجاست؟","texts":["تهران پایتخت ایران است.","موز یک میوه است."]}' 2>/dev/null)
|
| 65 |
+
TOP=$(echo "$R" | python3 -c "import sys,json;d=json.load(sys.stdin);d=sorted(d,key=lambda x:-x['score']);print(d[0]['index'])" 2>/dev/null)
|
| 66 |
+
[ "$TOP" = "0" ] && ok "relevant text ranked first" || no "rerank wrong/failed (top=$TOP, $R)"
|
| 67 |
+
|
| 68 |
+
hdr "7) Open WebUI reachable"
|
| 69 |
+
CODE=$(curl -s -o /dev/null -w "%{http_code}" "$OWUI/health" 2>/dev/null)
|
| 70 |
+
[ "$CODE" = "200" ] && ok "Open WebUI /health 200" || no "Open WebUI not healthy (HTTP $CODE)"
|
| 71 |
+
|
| 72 |
+
echo; echo "=================================================="
|
| 73 |
+
echo " RESULT: $PASS passed, $FAIL failed"
|
| 74 |
+
echo "=================================================="
|
| 75 |
+
[ "$FAIL" -eq 0 ]
|