homelab-codex-ws/services/ollama-piha/README.md
oskar e7625cd322 feat(kb): aktywny fallback embeddingów SOLARIA→PIHA dla kb-query (faza 4 Krok 2)
Ostatni krok fazy 4 KB (plan §2 Decyzja 2, §5): kb-query przestaje być martwe
przez ~16 h/dobę, gdy SOLARIA (GPU) śpi — zapytania embeduje wtedy lokalna
Ollama CPU na PIHA (wolniej: ~790 ms+ vs ~207 ms na GPU, ale działa).

Nowy serwis services/ollama-piha (GitOps, owner_node: piha):
- ollama/ollama:latest (arm64 natywnie), OLLAMA_KEEP_ALIVE=0 — model zwalnia
  RAM natychmiast po każdym wywołaniu (spike, nie rezydent; PIHA dzieli 8 GB z HA)
- bind wyłącznie 127.0.0.1 + LAN_BIND_IP (192.168.31.5), nigdy 0.0.0.0/Tailscale
- named volume ollama_piha_models (NVMe data-root) zamiast bind-mounta — obraz
  biega jako root w kontenerze i bind łamałby wzorzec uid PIHA (oskar=1004,
  kontenery uid 1000, setgid pi)
- override hosts/piha/runtime/ollama-piha: mem_limit 2560m (wartość startowa
  z planu, do potwierdzenia kalibracją na żywo), świadomie bez mem_reservation
- pull bge-m3 to jawny, ręczny krok deployu (README) — obraz nie ma modeli

kb-query — maszyna stanów fallbacku (app/embed_router.py):
- health-check SOLARII (GET /api/tags, timeout 1.5 s) z cache 30 s — zero
  sondowania per request; po powrocie SOLARII ruch wraca na GPU w ≤30 s
- primary up → embed na SOLARII z twardym timeoutem 3 s; błąd W TRAKCIE
  zapytania = jednorazowe przełączenie (krok 3b planu): status down na 30 s
  i TO SAMO zapytanie leci na fallback — user nie widzi błędu SOLARII
- primary down → embed prosto na ollama-piha (bez twardego timeoutu: CPU +
  zimny load modelu to legalnie pojedyncze sekundy)
- 503 tylko gdy oba backendy padłe (lub fallback nieskonfigurowany)
- inwariant modelu, druga połowa: każdy backend weryfikowany raz, leniwie przy
  pierwszym użyciu, że /api/tags zawiera EMBED_MODEL (bge-m3 — ta sama wartość
  co startowy check przeciw document_chunk.model/document_summary.embedding_model);
  niezgodność = ERROR log + 500, nigdy ciche liczenie dystansów między
  różnymi przestrzeniami embeddingów; leniwie, bo śpiąca SOLARIA nie może
  blokować startu serwisu
- odpowiedź /search: nowe pole embed_backend ("solaria"|"piha") + sol_status
  wg realnego świata routera (UI już renderuje down jako "offline (fallback
  embed)"); log INFO backend=... elapsed_ms=... per zapytanie
- /healthz: sol_status przez cache routera (spójny widok z routingiem) +
  fallback_status (żywa, tania sonda /api/tags)

Konfiguracja spójnie przez env (compose + env.example + service.yaml + README):
EMBED_PRIMARY_URL (zastępuje OLLAMA_URL), EMBED_FALLBACK_URL (pusty = brak
fallbacku, zachowanie sprzed kroku 2), EMBED_{PRIMARY,FALLBACK}_NAME,
EMBED_HEALTH_TTL_S/EMBED_HEALTH_TIMEOUT_S/EMBED_PRIMARY_TIMEOUT_S.

Testy: 39 pass (14 nowych w test_embed_router.py: cache TTL, failover w trakcie
zapytania, powrót po TTL, oba padłe, mismatch modelu na primary i fallbacku,
tag "bge-m3:latest" vs "bge-m3"); docker build + smoke (importy + uvicorn do
guardu KB_DSN) OK; compose config OK dla obu stacków.

Deploy (Oskar, na PIHA z mastera po merge):
  cd ~/homelab-codex-ws && git pull
  # 1. ollama-piha
  cp services/ollama-piha/env.example services/ollama-piha/.env
  docker compose -f services/ollama-piha/docker-compose.yml \
    -f hosts/piha/runtime/ollama-piha/docker-compose.override.yml \
    --env-file services/ollama-piha/.env up -d
  docker exec ollama-piha ollama pull bge-m3     # ręczny krok, obowiązkowy
  services/ollama-piha/healthcheck.sh
  # 2. kb-query (dopisać fallback do istniejącego .env)
  echo 'EMBED_FALLBACK_URL=http://192.168.31.5:11434' >> services/kb-query/.env
  docker compose -f services/kb-query/docker-compose.yml \
    -f hosts/piha/runtime/kb-query/docker-compose.override.yml up -d --build
  services/kb-query/healthcheck.sh
  # (deploy-node.sh też podniesie oba serwisy z hosts/piha/services.yaml,
  #  ale pull bge-m3 i .env pozostają ręczne)
Weryfikacja: testy A/B/C w services/kb-query/README.md (backend=solaria przy
SOLARII online; backend=piha przy symulacji offline; powrót na GPU w ≤30 s).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:01:29 +02:00

77 lines
3.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ollama-piha
Local CPU Ollama on **PIHA**, serving exactly one purpose: the **fallback embed
backend** for `kb-query` while SOLARIA (the GPU node, ~16 h/day powered off)
sleeps. Model: `bge-m3` — the **same** model as SOLARIA's Ollama, because query
embeddings must live in the same vector space as the pgvector index
(`document_chunk.embedding VECTOR(1024)`); a different/smaller model is not an
option (module 5 phase 4 plan §2 decision 2).
Expected latency: bge-m3 embeds in ~207 ms on SOLARIA's GPU vs ~790 ms on x86
CPU; on the Pi 5 expect single seconds per query (plus model load, since the
model is never resident — see below). Slower but alive beats fast but dead.
## Design constraints
- **`OLLAMA_KEEP_ALIVE=0`** (pinned in compose): PIHA is the RAM-bound 8 GB box
shared with Home Assistant. The model is unloaded immediately after every
call — a transient ~1.52 GB spike per embed, ~100 MB idle daemon, never a
resident cost.
- **`mem_limit: 2560m`** (host override, `hosts/piha/runtime/ollama-piha/`):
hard cgroup ceiling, plan §2 D2 starting value. The cgroup OOM killer
restarts this container instead of the host OOM killer picking a victim
(which could be Home Assistant). Confirm/trim after live calibration.
- **Bind**: `127.0.0.1` + `LAN_BIND_IP` (192.168.31.5) only — kb-query calls it
over the host LAN interface (same pattern as kb-query → kb-postgres:5433).
Never `0.0.0.0`, never a Tailscale bind, no public ingress.
- **Storage**: Docker named volume `ollama_piha_models` (NVMe data-root), not a
bind mount — the ollama image runs as in-container root and would break
PIHA's uid pattern (host oskar=1004, containers uid 1000, setgid group pi)
if it wrote to a shared bind directory.
## Deploy (PIHA, master, after merge)
```bash
cd ~/homelab-codex-ws && git pull
cp services/ollama-piha/env.example services/ollama-piha/.env # LAN_BIND_IP
docker compose -f services/ollama-piha/docker-compose.yml \
-f hosts/piha/runtime/ollama-piha/docker-compose.override.yml \
--env-file services/ollama-piha/.env up -d
```
**Then pull the model — this does NOT happen automatically:**
```bash
docker exec ollama-piha ollama pull bge-m3
```
Verify:
```bash
services/ollama-piha/healthcheck.sh # checks container + API + bge-m3 present
time curl -s http://127.0.0.1:11434/api/embeddings \
-d '{"model":"bge-m3","prompt":"test kalibracyjny"}' | head -c 80
```
(`deploy-node.sh` on PIHA also picks this service up from
`hosts/piha/services.yaml` once `.env` exists — the `ollama pull bge-m3` step
stays manual either way.)
## Calibration (plan §5 step 4 — gate, not formality)
Before trusting the fallback under load, on live PIHA at a normal (not
night-quiet) hour: run a few embeds as above while watching
`docker stats ollama-piha`, note peak RAM and wall time. Verdict per plan §5
step 5: keep as default fallback / tune `mem_limit` / fall back to explicit
503 degradation.
## Relation to kb-query
kb-query's router (`services/kb-query/app/embed_router.py`) health-checks
SOLARIA with a ~30 s cache and only sends embeds here while SOLARIA is down.
kb-query verifies at first use that this backend actually serves `bge-m3`
(`/api/tags`) and refuses to embed against a mismatched model. Configuration:
`EMBED_FALLBACK_URL=http://192.168.31.5:11434` in `services/kb-query/.env`.
See `services/kb-query/README.md` for the fallback verification plan (tests
A/B/C).