Skip to content

Commit 1f44653

Browse files
committed
feat: add llama-server (llama.cpp) as alternative extraction backend
LlmExtractor already speaks OpenAI-compatible HTTP, so llama.cpp's llama-server is a drop-in alternative to Ollama via CTXGRAPH_LLM_URL. - scripts/run-llama-server.sh: serves a GGUF with configurable -ngl/ctx/port, reusing the blob Ollama already downloaded (resolved via `ollama show`) so there is no second copy on disk. Defaults (ngl=12, ctx=2048) fit a 6GB GPU with a desktop already using ~2GB. - docs/llm-backends.md: documents all three backends (Ollama, cloud, llama-server), the env-var contract, the test harness, and a local-model comparison table. Verified: llm_smoke_test passes through LlmExtractor against a CUDA-built llama-server (0/3 errors, parity with the Ollama path).
1 parent d7c29b1 commit 1f44653

2 files changed

Lines changed: 162 additions & 0 deletions

File tree

docs/llm-backends.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# LLM backends for extraction
2+
3+
`ctxgraph`'s LLM extraction path (`LlmExtractor`, used by the v0.3 universal
4+
pipeline) is a plain **OpenAI-compatible HTTP client**. It does not bind to any
5+
particular runtime — it POSTs to `/v1/chat/completions` and parses the JSON
6+
back. That means any of the following are drop-in backends, selected entirely by
7+
environment variables.
8+
9+
## Configuration
10+
11+
`LlmExtractor::from_env()` resolves a backend in this order:
12+
13+
1. **Explicit (Tier 0)**`CTXGRAPH_LLM_KEY` set → use `CTXGRAPH_LLM_URL` /
14+
`CTXGRAPH_LLM_MODEL` verbatim. Highest priority; this is how you force a
15+
specific local model.
16+
2. **Ollama auto-detect (Tier 1)** — probes `localhost:11434`, picks a preferred
17+
small model. Free, private, zero config.
18+
3. **Cloud (Tier 2)**`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `OPENROUTER_API_KEY`.
19+
20+
| Variable | Purpose | Default |
21+
|---|---|---|
22+
| `CTXGRAPH_LLM_KEY` | API key (any non-empty string for local servers) ||
23+
| `CTXGRAPH_LLM_URL` | Full `/v1/chat/completions` URL | provider-specific |
24+
| `CTXGRAPH_LLM_MODEL` | Model name sent in the request | `gpt-4o-mini` |
25+
| `CTXGRAPH_LLM_TIMEOUT` | Per-request timeout, seconds | 60 (Ollama 120) |
26+
| `CTXGRAPH_NO_LLM=1` | Disable the LLM path entirely ||
27+
| `CTXGRAPH_NO_OLLAMA=1` | Skip Ollama auto-detect ||
28+
29+
> Large local models partly offloaded to CPU can take **minutes** per episode.
30+
> Set `CTXGRAPH_LLM_TIMEOUT=600` so the client doesn't give up early.
31+
32+
## Backend A — Ollama (default, easiest)
33+
34+
```bash
35+
ollama pull hf.co/yuxinlu1/gemma-4-12B-coder-fable5-composer2.5-v1-GGUF:Q4_K_M
36+
env CTXGRAPH_LLM_KEY=ollama \
37+
CTXGRAPH_LLM_URL=http://localhost:11434/v1/chat/completions \
38+
CTXGRAPH_LLM_MODEL='hf.co/yuxinlu1/gemma-4-12B-coder-fable5-composer2.5-v1-GGUF:Q4_K_M' \
39+
CTXGRAPH_LLM_TIMEOUT=600 \
40+
cargo test -p ctxgraph-extract --test llm_cross_domain_test -- --ignored --nocapture
41+
```
42+
43+
## Backend B — llama.cpp `llama-server` (more control)
44+
45+
Use this when you want direct control over GPU offload (`-ngl`), context size,
46+
flash attention, or grammar-constrained output. `scripts/run-llama-server.sh`
47+
serves a GGUF — by default **reusing the blob Ollama already downloaded**, so
48+
there's no second copy on disk:
49+
50+
```bash
51+
# terminal 1 — serve (resolves the GGUF from the ollama model tag)
52+
scripts/run-llama-server.sh # default coder model, port 8080
53+
# or: GGUF=/path/to/model.gguf NGL=24 PORT=8080 scripts/run-llama-server.sh
54+
55+
# terminal 2 — point ctxgraph at it
56+
env CTXGRAPH_LLM_KEY=llamacpp \
57+
CTXGRAPH_LLM_URL=http://127.0.0.1:8080/v1/chat/completions \
58+
CTXGRAPH_LLM_MODEL=local \
59+
CTXGRAPH_LLM_TIMEOUT=600 \
60+
cargo test -p ctxgraph-extract --test llm_cross_domain_test -- --ignored --nocapture
61+
```
62+
63+
The script's header documents how to build a CUDA `llama-server` into
64+
`~/.local/bin` (no sudo needed if the CUDA toolkit is installed).
65+
66+
## Testing a model: the harness
67+
68+
`tests/llm_cross_domain_test.rs` exercises `LlmExtractor` directly (not the
69+
GLiNER ONNX pipeline) so you can score any backend:
70+
71+
- `llm_smoke_test` — 3 off-fixture real-world snippets; asserts the model
72+
returns usable extraction JSON at all.
73+
- `llm_cross_domain_hard_test` — per-domain entity + relation F1 across the 6
74+
`cross_domain_episodes.json` domains. Exploratory (no threshold asserted);
75+
never panics on bad model output — extraction errors are counted and reported.
76+
77+
## Reference: local-model comparison
78+
79+
Same harness, fixtures, and fuzzy-F1 scorer across all 10 cross-domain episodes
80+
on a 6 GB-VRAM laptop (RTX 4050, partial CPU offload via Ollama):
81+
82+
| Model | Entity F1 (name) | Entity F1 (strict) | Relation F1 | Combined | Errors | Wall time |
83+
|---|---|---|---|---|---|---|
84+
| gemma-4-12B-coder (Q4_K_M) | 0.816 | 0.492 | 0.398 | 0.607 | 0/10 | ~20 min |
85+
| qwen2.5:7b-instruct (Q4_K_M) | 0.850 | 0.383 | 0.263 | 0.557 | 0/10 | ~2.4 min |
86+
| Gemma-4-12B-OBLITERATED (Q4_K_M) | 0.083 | 0.067 | 0.100 | 0.092 | 9/10 | ~105 min |
87+
88+
Takeaways:
89+
90+
- **Entity recall is the strong suit** of every working model; **relation F1 and
91+
strict (name+type) F1 are the weak spots** — partly real model error, partly
92+
the scorer requiring exact head/tail entity-name matches.
93+
- **The coder fine-tune did not beat a stock 7B instruct** in a way that justifies
94+
8× the latency: qwen2.5:7b matches its entity recall and fits entirely in VRAM.
95+
The coder's code bias even leaks `snake_case` entity names that break relation
96+
matching (worst on the finance domain).
97+
- **Abliterated models are unusable here**`Gemma-4-12B-OBLITERATED` returned
98+
empty/unparseable output on 9/10 episodes. Abliteration strips the
99+
instruction-following that structured-JSON extraction relies on.

scripts/run-llama-server.sh

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env bash
2+
# Run llama.cpp's `llama-server` as an OpenAI-compatible backend for ctxgraph.
3+
#
4+
# ctxgraph's LlmExtractor speaks plain OpenAI-compatible HTTP, so anything that
5+
# exposes POST /v1/chat/completions works as a drop-in backend. This serves a
6+
# local GGUF with llama-server and prints the env vars that point ctxgraph at it.
7+
#
8+
# By default it REUSES the GGUF that Ollama already downloaded (no second 7GB
9+
# copy on disk): it resolves the blob path from an Ollama model tag via
10+
# `ollama show <tag> --modelfile`. Override with GGUF=/path/to/model.gguf.
11+
#
12+
# Prereq: a `llama-server` binary on PATH. Build one (CUDA) with:
13+
# git clone --depth 1 https://github.com/ggml-org/llama.cpp ~/.local/src/llama.cpp
14+
# cd ~/.local/src/llama.cpp
15+
# CUDACXX=/opt/cuda/bin/nvcc cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
16+
# -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=89 -DLLAMA_CURL=OFF
17+
# cmake --build build --target llama-server -j"$(nproc)"
18+
# cp build/bin/llama-server ~/.local/bin/
19+
#
20+
# Usage:
21+
# scripts/run-llama-server.sh # default coder model
22+
# OLLAMA_MODEL=qwen2.5:7b-instruct-q4_K_M scripts/run-llama-server.sh
23+
# GGUF=/path/to/model.gguf NGL=33 PORT=8080 scripts/run-llama-server.sh
24+
set -euo pipefail
25+
26+
PORT="${PORT:-8080}"
27+
HOST="${HOST:-127.0.0.1}"
28+
NGL="${NGL:-12}" # GPU layers to offload. Tune to FREE VRAM, not total:
29+
# ~12 layers of a 12B Q4_K_M fits when a desktop already
30+
# uses ~2GB of a 6GB GPU. Raise it when more VRAM is free.
31+
CTX="${CTX:-2048}" # context window (KV cache grows with this)
32+
OLLAMA_MODEL="${OLLAMA_MODEL:-hf.co/yuxinlu1/gemma-4-12B-coder-fable5-composer2.5-v1-GGUF:Q4_K_M}"
33+
34+
# --- resolve the GGUF path -------------------------------------------------
35+
if [ -n "${GGUF:-}" ]; then
36+
MODEL_PATH="$GGUF"
37+
else
38+
echo "Resolving GGUF for ollama model: $OLLAMA_MODEL" >&2
39+
MODEL_PATH="$(ollama show "$OLLAMA_MODEL" --modelfile | awk '/^FROM /{print $2; exit}')"
40+
fi
41+
[ -r "$MODEL_PATH" ] || { echo "ERROR: GGUF not readable: $MODEL_PATH" >&2; exit 1; }
42+
command -v llama-server >/dev/null 2>&1 || {
43+
echo "ERROR: llama-server not on PATH — build it first (see header)." >&2; exit 1; }
44+
45+
echo "Serving: $MODEL_PATH" >&2
46+
echo >&2
47+
echo "Point ctxgraph at this backend (in another shell):" >&2
48+
cat >&2 <<EOF
49+
export CTXGRAPH_LLM_KEY=llamacpp
50+
export CTXGRAPH_LLM_URL=http://${HOST}:${PORT}/v1/chat/completions
51+
export CTXGRAPH_LLM_MODEL=local
52+
export CTXGRAPH_LLM_TIMEOUT=600
53+
EOF
54+
echo >&2
55+
56+
# --jinja uses the GGUF's embedded chat template; --alias fixes the model name
57+
# the OpenAI endpoint reports (so CTXGRAPH_LLM_MODEL=local lines up).
58+
exec llama-server \
59+
-m "$MODEL_PATH" \
60+
--host "$HOST" --port "$PORT" \
61+
-ngl "$NGL" -c "$CTX" \
62+
--jinja \
63+
--alias local

0 commit comments

Comments
 (0)