# llama.cpp Deployment Patterns

## CPU-only build (CUDA toolkit mismatch workaround)

When nvcc and CUDA libraries are from different versions (e.g., nvcc 12.4, libcublas.so from CUDA 13.1), the CUDA build will fail with linker errors like `undefined reference to cublasGemmEx@libcublas.so.12`.

**Fix:** Build CPU-only. For models ≤3B parameters, CPU inference is fast enough (20-40 tok/s).

```bash
git clone --depth 1 --branch <tag> https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j4
```

## Systemd service

```ini
[Unit]
Description=llama.cpp server for <model>
After=network.target

[Service]
Type=simple
User=<user>
ExecStart=<path>/llama-server \
    -m <path>/model.gguf \
    -c 4096 \
    --host 127.0.0.1 \
    --port 8081 \
    -t 4
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
```

Install:
```bash
sudo cp llama-server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now llama-server
```

## Nginx reverse proxy

For browser access to llama-server (avoids CORS, works across devices):

```nginx
location /llm/ {
    proxy_pass http://127.0.0.1:8081/;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_read_timeout 120s;
}
```

Test: `curl -sk https://your-domain:port/llm/health`

## Browser integration

```javascript
const resp = await fetch('/llm/v1/chat/completions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        messages: [
            {role:'system', content:'Your prompt'},
            {role:'user', content: userText}
        ],
        temperature: 0,
        max_tokens: 500
    })
});
const data = await resp.json();
let raw = data.choices[0].message.content;
// Strip markdown fences (small models often wrap JSON)
raw = raw.replace(/^```(?:json)?\s*\n?/i, '').replace(/\n?```\s*$/i, '');
const parsed = JSON.parse(raw);
```

## OCR → LLM pipeline pattern

1. Tesseract.js runs OCR in browser → raw text
2. Browser POSTs text to `/llm/v1/chat/completions`
3. llama-server processes → returns structured JSON
4. Client normalizes fields (strip phone dashes, uppercase VIN)
5. Fallback to rule-based parser if LLM unavailable

## Prompt engineering for structured extraction

Key rules for reliable JSON extraction from small models (1.5B-3B):
- Return ONLY JSON, no markdown — but strip fences anyway
- Explicit field rules in system prompt (not just schema)
- Include example values in field descriptions
- Use temperature=0 for deterministic output
- `max_tokens=500` sufficient for 3-5 appointment records

Example system prompt for OCR appointment parsing:
```
Extract appointment details from OCR text. Return ONLY valid JSON, no markdown.

RULES:
- customerName: person name before phone number. Strip "RO" prefix.
- customerPhone: 10 digits only, no dashes
- customerEmail: actual email with @
- vin: 17-char uppercase VIN. OCR may misread 0 as O, 1 as I.
- vehicleInfo: year + make + model
- serviceType: work description after opcode
- opCode: only bracketed code like [REP] — no advisor names
- appointmentTime: 24h format (e.g. "09:00")
- duration: integer minutes

JSON: {"appointments":[{"customerName":"","customerPhone":"","customerEmail":"","vin":"","vehicleInfo":"","serviceType":"","opCode":"","appointmentTime":"","duration":0}]}
```

## Socket-activated llama-server (VRAM on-demand)

When the GPU is shared with other workloads (gaming, other models), keeping the model in VRAM 24/7 wastes resources. systemd socket activation starts llama-server only when a request hits the port, and auto-stops it after idle timeout.

**Cold-start latency:** 7-8s for a ~9 GB GGUF on PCIe 3.0 + NVMe (NVMe read ~3.5s, PCIe transfer ~0.7s, CUDA init ~3s). Warm restart (file cached in RAM): 3-4s. Acceptable for batch OCR and non-interactive use.

### Socket file

```ini
# /etc/systemd/system/llama-server.socket
[Socket]
ListenStream=127.0.0.1:8081

[Install]
WantedBy=sockets.target
```

### Service file (modified)

```ini
[Unit]
Description=llama.cpp server (socket-activated)
After=network.target

[Service]
Type=simple
User=ray
WorkingDirectory=/home/ray
ExecStart=/path/to/llama-server \
    -m /path/to/model.gguf \
    -c 4096 \
    --host 127.0.0.1 \
    --port 8081 \
    -ngl 99 \
    -t 8
Restart=on-failure
RestartSec=5
StopIdleSec=300

[Install]
WantedBy=multi-user.target
```

**`StopIdleSec=300`** — systemd kills the service after 5 minutes with no active connections. Adjust up/down as needed.

### Activation

```bash
sudo systemctl stop llama-server
sudo systemctl disable llama-server
sudo systemctl enable --now llama-server.socket
# Now llama-server only launches when :8081 receives a connection
```

**Verification:** `ss -tlnp | grep 8081` shows the socket in LISTEN state with systemd as the listener. First request triggers service start.

### VRAM lifecycle

| State | VRAM |
|---|---|
| Idle (no connections) | 0 MB (service not running) |
| Active (handling requests) | Model size (~3-9 GB depending on quant) |
| Post-idle (StopIdleSec elapsed) | 0 MB (service killed, VRAM freed) |

### When NOT to socket-activate

- Interactive chat where 7-8s cold start is annoying
- Frequent bursts of requests (model reloads repeatedly)
- When the GPU has enough VRAM to leave the model resident permanently (e.g., 24 GB card with a 7B model)

## Multi-model deployment

Running two llama-server instances on different ports for different purposes (e.g., small model for chat + larger model for delegation/coding).

```bash
# Instance 1: main model (port 8081)
llama-server -m Qwen2.5-7B-Instruct-Q4_K_M.gguf -c 4096 --port 8081 -ngl 99 -t 8 &

# Instance 2: delegation/coding model (port 8082)
llama-server -m Qwen2.5-Coder-14B-Instruct-Q4_K_M.gguf -c 16384 --port 8082 -ngl 99 -t 8 &
```

### VRAM budgeting for dual models

Must fit within total GPU VRAM. Example on RTX 2080 Ti (11 GB):

| Combination | VRAM used | Fits? |
|---|---|---|
| 7B Q2_K (3.2 GB) + 7B Q4_K_M (4.7 GB) | ~7.9 GB | ✓ |
| 7B Q2_K (3.2 GB) + 14B Q4_K_M (9 GB) | ~12.2 GB | ✗ |
| 3B Q4_K_M (2 GB) + 14B Q4_K_M (9 GB) | ~11 GB | Tight |

For dual-model on a single GPU, prefer socket activation on one instance so they don't both stay loaded. Or accept that only one runs at a time.

### Hermes delegation config

Point Hermes subagents at a local delegation model:

```yaml
# ~/.hermes/config.yaml
delegation:
  provider: custom:local-delegation
  model: qwen-coder-14b
  base_url: http://127.0.0.1:8082/v1
  api_key: not-needed
  max_concurrent_children: 1  # local model can't parallelize well
```

Any OpenAI-compatible client can be pointed at llama-server the same way — set `base_url` to `http://127.0.0.1:<port>/v1` and use a placeholder API key.

### Quality expectations for delegation

A local 14B Q4 model vs a cloud API (e.g., DeepSeek V4 Flash):

| Dimension | Local 14B Q4 | Cloud API |
|---|---|---|
| Structured tasks (file ops, patches, grep) | Good | Excellent |
| Simple debugging | Good | Excellent |
| Complex multi-step reasoning | Fair | Excellent |
| Code generation (new features) | Good | Very good |
| Cost per delegation | $0 | API tokens |
| Latency | 45-55 tok/s local | API round-trip |

For delegation workloads (which are mostly structured), a 14B at Q4 is competent. The quality gap is real but often acceptable for the cost savings.

- GPU: GTX 1050 Ti 4GB VRAM
- RAM: 14GB
- Qwen2.5-1.5B-Instruct Q4_K_M: 941 MB, ~30 tok/s CPU, loads in ~500ms
- Llama-3.2-3B-Instruct Q4_K_M: ~2 GB, would fit in VRAM if CUDA worked
