Neohosseinism Claude Fable 5 commited on
Commit
476ec1a
Β·
1 Parent(s): 7544c3e

Fix multi-minute UI latency: disable reasoning, preload model, kill hidden task calls

Browse files

Browser-measured TTFT through Open WebUI went from ~210s to ~2.9s:

- llama-swap macro: --reasoning off (unbounded thinking ate the whole
budget with zero visible output; budget 0 leaks thinking prose into
content), -n 1024 output cap, --cache-reuse 256 (multi-turn prompt
cache, ~70% less prompt cost on follow-ups).
- Preload DEFAULT_MODEL at startup (hooks.on_startup) and raise ttl
600->86400 so the first message never pays the ~60s cold load.
- Open WebUI: disable title/tags/follow-up/autocomplete/query
generation - each fired an extra LLM call that queued the real chat
on the single CPU slot. NB these are PersistentConfig: env vars are
ignored once values exist in webui.db (fixed live via
/api/v1/tasks/config/update; envs kept for fresh installs).
- README: measured perf on the 4c/16GB box + capacity planning
estimates for 8c/16GB and 16c/64GB tiers (speed, TTFT, RPS, RAM
budgets, parallel-slot notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

README.md CHANGED
@@ -21,7 +21,8 @@ Open WebUI ──┬─► llama-swap ─► llama-server (Gemma 4 omni + BF16 m
21
 
22
  Requests flow **User β†’ Open WebUI β†’ {llama-swap, tei-embed, tei-rerank}**; llama-swap in turn
23
  spawns `llama-server` on demand for whichever Gemma 4 profile is selected (only one resident at a
24
- time, unloaded on idle-ttl to keep RAM flat). The audio Pipe bypasses Open WebUI's built-in STT
 
25
  and talks to llama-swap directly so Gemma hears raw audio natively. Dotted arrows are read/write
26
  volume mounts, not network calls.
27
 
@@ -90,8 +91,6 @@ docker compose up -d # boots TEI + Open WebUI + llama-swap
90
  ```
91
  Open <http://localhost:3000>, create the first (admin) account.
92
 
93
- Open <http://localhost:3000>, create the first (admin) account.
94
-
95
  **Default Open WebUI login:**
96
  - **Email:** `UI@gmail.com`
97
  - **Password:** `G4H!dg!R!4EdjkR`
@@ -149,6 +148,66 @@ B64=$(base64 -w0 clip.wav); curl localhost:8080/v1/chat/completions -H 'Content-
149
  to sweep `--threads`. Set the winner as `THREADS` in `.env`. Long context? add KV quant
150
  (`-ctk q8_0 -ctv q8_0`) to the macro in `llama-swap/config.tmpl.yaml`.
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  ## Troubleshooting (the known traps)
153
  - **Audio crashes / asserts** β†’ keep `-b 2048 -ub 2048` (already in the macro). Default 512 crashes.
154
  - **Audio is garbage/repetitive** β†’ the mmproj must be **BF16** (we pin it). Q8_0/F16 break audio.
@@ -163,9 +222,11 @@ to sweep `--threads`. Set the winner as `THREADS` in `.env`. Long context? add K
163
  `RAG_RERANKING_ENGINE=external` + URL envs and set `RAG_RERANKING_MODEL=BAAI/bge-reranker-v2-m3`.
164
 
165
  ## Operational notes (validated live on a 15 GB CPU box)
166
- - **Gemma 4 is a reasoning model** β€” it emits `reasoning_content` before `content`. Give it a
167
- generous `max_tokens` (β‰₯256) or short prompts will return empty `content` (all budget spent
168
- thinking). The Pipe/UI handle this; raw API callers should read both fields.
 
 
169
  - **Send media via a file, not inline** β€” base64 audio/large images exceed the shell arg limit
170
  (`argument list too long`). Use `curl -d @payload.json` (the e2e script and Pipe already do).
171
  - **TEI memory on CPU**: the ONNX backend is heavy (bge-m3 β‰ˆ 4.75 GB). Setting `TEI_DTYPE=float16`
 
21
 
22
  Requests flow **User β†’ Open WebUI β†’ {llama-swap, tei-embed, tei-rerank}**; llama-swap in turn
23
  spawns `llama-server` on demand for whichever Gemma 4 profile is selected (only one resident at a
24
+ time; the profile's `DEFAULT_MODEL` is preloaded at startup and kept warm for 24 h so the first
25
+ message never pays the ~60 s cold load β€” switching models still swaps on demand). The audio Pipe bypasses Open WebUI's built-in STT
26
  and talks to llama-swap directly so Gemma hears raw audio natively. Dotted arrows are read/write
27
  volume mounts, not network calls.
28
 
 
91
  ```
92
  Open <http://localhost:3000>, create the first (admin) account.
93
 
 
 
94
  **Default Open WebUI login:**
95
  - **Email:** `UI@gmail.com`
96
  - **Password:** `G4H!dg!R!4EdjkR`
 
148
  to sweep `--threads`. Set the winner as `THREADS` in `.env`. Long context? add KV quant
149
  (`-ctk q8_0 -ctv q8_0`) to the macro in `llama-swap/config.tmpl.yaml`.
150
 
151
+ ## Performance & capacity planning (CPU serving)
152
+
153
+ **Measured** on the 4-core / 16 GB dev box (Xeon 2.6 GHz, E4B Q4_K_M, warm model,
154
+ via the real Open WebUI through the public proxy β€” not just raw llama-bench):
155
+
156
+ | Metric (E4B, warm) | 4c / 16 GB β€” measured |
157
+ |---|---|
158
+ | TTFT in the browser, short prompt | **~3 s** |
159
+ | Generation speed | **~3.4–4.0 tok/s** |
160
+ | Prompt processing | **~8–15 tok/s** |
161
+ | ~150-token chat answer, end to end | **~45–70 s** |
162
+ | Follow-up turn (prompt cache hit) | prompt cost β‰ˆ **–70 %** |
163
+ | TEI embed / rerank (per request) | **0.7 s / 1.7 s** |
164
+
165
+ **Back-of-envelope formula** (holds well in practice):
166
+
167
+ ```
168
+ TTFT β‰ˆ prompt_tokens / pp_speed (+ ~0.5 s overhead)
169
+ total β‰ˆ TTFT + output_tokens / gen_speed
170
+ RPS β‰ˆ parallel_slots / total (sequential queue otherwise)
171
+ ```
172
+
173
+ **Estimates for bigger CPU tiers** (prompt processing is compute-bound β†’ scales ~linearly
174
+ with physical cores; token generation is memory-bandwidth-bound β†’ scales sublinearly,
175
+ roughly Γ—1.7 at 8c and Γ—2.5–3 at 16c vs this box). Assumes a ~300-token prompt and
176
+ ~150-token answer per request:
177
+
178
+ | | 4c / 16 GB (this box) | 8c / 16 GB (est.) | 16c / 64 GB (est.) |
179
+ |---|---|---|---|
180
+ | **E4B** gen / pp (tok/s) | 3.5–4 / 8–15 βœ“ | ~6–7 / ~25 | ~9–12 / ~50 |
181
+ | **E4B** TTFT / total per chat | ~3 s / ~60 s βœ“ | ~1.5 s / ~35 s | ~1 s / ~20 s |
182
+ | **E4B** chat throughput | ~1 req/min (RPS β‰ˆ 0.015) | ~1.7 req/min (0.03) | ~3 req/min (0.05) |
183
+ | **12B** gen / pp (tok/s) | ~1.2–1.5 / ~4 (RAM-tight) | ~2–2.5 / ~9 (tight) | ~4–5 / ~20 |
184
+ | **12B** chat total / throughput | not recommended | ~2 min/req | ~55 s/req, ~1 req/min |
185
+ | **26B-A4B** (MoE, ~18 GB) | βœ— no RAM | βœ— no RAM | ~7–9 / ~40; ~30 s/req |
186
+ | Comfortable interactive users | 1 | 1–2 | 2–4 |
187
+
188
+ Practical read of that table:
189
+
190
+ - **16 GB RAM caps you at E4B** as the daily driver (12B fits but leaves TEI+UI almost no
191
+ headroom). **The 16c / 64 GB tier is the first one where 12B and 26B-A4B are pleasant.**
192
+ - **Concurrency**: `llama-server` runs **one inference slot** by default β€” a second chat
193
+ *queues*, it doesn't run alongside. On the 16c/64 GB tier add `--parallel 2` (and note each
194
+ slot gets `CTX / N` context) to serve 2 streams; aggregate tok/s barely changes
195
+ (bandwidth-bound), it just shares fairly instead of queueing.
196
+ - **CPU serving is single-digit-users territory.** For ~10+ concurrent chat users at
197
+ interactive speeds, use the GPU profile β€” no CPU tier gets you there.
198
+ - **RAM budget** (Q4_K_M + BF16 mmproj + 8k ctx): E4B β‰ˆ 5 GB Β· 12B β‰ˆ 10 GB Β· 26B-A4B β‰ˆ 18 GB;
199
+ plus TEI embed+rerank β‰ˆ 4 GB, Open WebUI β‰ˆ 0.7 GB, OS β‰ˆ 1 GB.
200
+ - Embedding RAG load is cheap next to chat: TEI on this box sustains β‰ˆ 1.4 embeds/s and
201
+ β‰ˆ 0.6 reranks/s per core-set β€” RAG retrieval adds ~2–3 s per message, not minutes.
202
+
203
+ **Latency defaults already baked into this stack** (the difference between these numbers and
204
+ minutes-long hangs): `--reasoning off` (Gemma 4 otherwise burns the whole budget thinking),
205
+ `-n 1024` output cap, `--cache-reuse 256` (multi-turn prompt cache), startup **preload** of
206
+ `DEFAULT_MODEL` + 24 h idle-ttl (no cold load on first message), and Open WebUI background
207
+ task generation (title/tags/follow-up/query) **disabled** β€” note those are PersistentConfig:
208
+ once in `webui.db`, env vars are ignored; change them in Admin Settings β†’ Interface or via
209
+ `POST /api/v1/tasks/config/update`.
210
+
211
  ## Troubleshooting (the known traps)
212
  - **Audio crashes / asserts** β†’ keep `-b 2048 -ub 2048` (already in the macro). Default 512 crashes.
213
  - **Audio is garbage/repetitive** β†’ the mmproj must be **BF16** (we pin it). Q8_0/F16 break audio.
 
222
  `RAG_RERANKING_ENGINE=external` + URL envs and set `RAG_RERANKING_MODEL=BAAI/bge-reranker-v2-m3`.
223
 
224
  ## Operational notes (validated live on a 15 GB CPU box)
225
+ - **Gemma 4 is a reasoning model** β€” by default it emits `reasoning_content` before `content`
226
+ and, unbounded, can think for *minutes* before a visible answer. The macro therefore ships
227
+ `--reasoning off` (clean direct answers). If you re-enable it, prefer `--reasoning-budget N`
228
+ over unbounded, give `max_tokens` β‰₯ 256, and read both fields in raw API callers
229
+ (`--reasoning-budget 0` leaks "Thinking Process:" prose into `content` β€” use `off`).
230
  - **Send media via a file, not inline** β€” base64 audio/large images exceed the shell arg limit
231
  (`argument list too long`). Use `curl -d @payload.json` (the e2e script and Pipe already do).
232
  - **TEI memory on CPU**: the ONNX backend is heavy (bge-m3 β‰ˆ 4.75 GB). Setting `TEI_DTYPE=float16`
docker-compose.yml CHANGED
@@ -32,6 +32,7 @@ services:
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
@@ -131,6 +132,13 @@ services:
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
 
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
+ DEFAULT_MODEL: ${DEFAULT_MODEL:-gemma-e4b} # preloaded at startup (no cold first message)
36
  HF_TOKEN: ${HF_TOKEN:-} # for gated Gemma 4 pulls via -hf
37
  HF_HOME: /models/hf-cache
38
  LLAMA_CACHE: /models/llama-cache # persist GGUFs fetched by -hf across restarts
 
132
  RAG_TOP_K_RERANKER: "4"
133
  # --- native audio is the Pipe's job; turn STT off ----------------------
134
  AUDIO_STT_ENGINE: ""
135
+ # --- kill hidden background LLM calls (title/tags/follow-up/autocomplete
136
+ # each fire an extra chat completion on the same slow CPU backend) ---
137
+ ENABLE_TITLE_GENERATION: "false"
138
+ ENABLE_TAGS_GENERATION: "false"
139
+ ENABLE_FOLLOW_UP_GENERATION: "false"
140
+ ENABLE_AUTOCOMPLETE_GENERATION: "false"
141
+ ENABLE_RETRIEVAL_QUERY_GENERATION: "false"
142
  WEBUI_NAME: ${WEBUI_NAME:-Gemma 4 Local}
143
  volumes:
144
  - ./openwebui/data:/app/backend/data
llama-swap/config.tmpl.yaml CHANGED
@@ -16,12 +16,21 @@
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:
@@ -32,7 +41,7 @@ models:
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":
@@ -41,7 +50,7 @@ models:
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":
@@ -50,4 +59,4 @@ models:
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
 
16
  healthCheckTimeout: 300
17
  logLevel: info
18
 
19
+ # Warm the profile's default model as soon as the stack starts, so the first
20
+ # chat message doesn't pay the ~60s cold-load.
21
+ hooks:
22
+ on_startup:
23
+ preload:
24
+ - "${DEFAULT_MODEL}"
25
+
26
  macros:
27
  base: >-
28
  llama-server --host 0.0.0.0 --port ${PORT}
29
  --jinja --no-mmproj-offload -ngl ${NGL}
30
  --threads ${THREADS} -b 2048 -ub 2048
31
  --temp 1.0 --top-k 64 --top-p 0.95
32
+ --reasoning off -n 1024
33
+ --cache-reuse 256
34
  --metrics
35
 
36
  models:
 
41
  -m /models/gemma-4-E4B-it-Q4_K_M.gguf
42
  --mmproj /models/mmproj-gemma-4-E4B-it-bf16.gguf
43
  -c ${CTX}
44
+ ttl: 86400
45
 
46
  # ---- 12B: audio + image + text (server default, ~10-12 GB) ----------------
47
  "gemma-12b":
 
50
  -hf ggml-org/gemma-4-12B-it-GGUF:Q4_K_M
51
  --mmproj /models/mmproj-gemma-4-12B-it-bf16.gguf
52
  -c ${CTX}
53
+ ttl: 86400
54
 
55
  # ---- 26B-A4B MoE: image + text ONLY, no audio (~16-18 GB) -----------------
56
  "gemma-26b-a4b":
 
59
  -hf ggml-org/gemma-4-26B-A4B-it-GGUF:Q4_K_M
60
  --mmproj /models/mmproj-gemma-4-26B-A4B-it-bf16.gguf
61
  -c ${CTX}
62
+ ttl: 86400
llama-swap/entrypoint.sh CHANGED
@@ -7,6 +7,7 @@ set -eu
7
  NGL="${NGL:-0}"
8
  THREADS="${THREADS:-4}"
9
  CTX="${CTX:-8192}"
 
10
 
11
  TMPL="/config/config.tmpl.yaml"
12
  OUT="/tmp/config.yaml"
@@ -15,6 +16,7 @@ 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 "===================================================================="
 
7
  NGL="${NGL:-0}"
8
  THREADS="${THREADS:-4}"
9
  CTX="${CTX:-8192}"
10
+ DEFAULT_MODEL="${DEFAULT_MODEL:-gemma-e4b}"
11
 
12
  TMPL="/config/config.tmpl.yaml"
13
  OUT="/tmp/config.yaml"
 
16
  -e "s|\${NGL}|${NGL}|g" \
17
  -e "s|\${THREADS}|${THREADS}|g" \
18
  -e "s|\${CTX}|${CTX}|g" \
19
+ -e "s|\${DEFAULT_MODEL}|${DEFAULT_MODEL}|g" \
20
  "$TMPL" > "$OUT"
21
 
22
  echo "===================================================================="