# CPU Background Service Preservation (May 2026)

## Problem
Background CPU-only models (SmolLM3-3B-CPU on port 8087, ngl 0) were being incorrectly stopped during GPU model switches because `get_active_service()` iterates ALL MODELS and returns them as "active".

## Root Cause
The model-switcher's `swap()` function stopped whatever `get_active_service()` returned without distinguishing GPU vs CPU models. The issue was compounded by the fact that `get_active_service()` returns the **first** active model in MODELS order — since SmolLM3 is always running, it would be found first and the function would return immediately, never reaching the GPU model.

Background services running independently (not in GPU VRAM, on CPU-only ports like 8087) should never be disrupted.

## Solution: `"preserve": True` pattern + get_active_service() fix

### models.py — mark CPU-only models
```python
{
    "name": "SmolLM3-3B-CPU",
    "desc": "SmolLM3 3B CPU (Hindsight)",
    "service": "llama-server-smol",
    "preserve": True,  # <-- never stopped during GPU switches
    ...
}
```

### auto-model-switch.py and model-switcher — skip preserved in get_active_service()
The guard in `swap()` is not sufficient alone — `get_active_service()` must also skip preserved models, otherwise it returns the CPU service first and the `swap()` function never finds the active GPU model:

```python
def get_active_service():
    """Return the first non-preserved active service."""
    for model in MODELS:
        if model.get("preserve"):
            continue  # <-- skip CPU-only background services
        r = subprocess.run(["systemctl", "is-active", model["service"]], ...)
        if r.stdout.strip() == "active":
            return model
    return None
```

Both `model-switcher` and `auto-model-switch.py` must have this fix independently.

## Pitfall: Python `True` vs JSON `true`
Models.py is a Python file, not JSON. Boolean literals must be capitalized: `True` not `true`. Writing `true` causes a `NameError: name 'true' is not defined`.

## Pitfall: Two scripts, two fixes needed
The preserve-skip logic must be applied in BOTH `model-switcher` and `auto-model-switch.py`. They have independent `get_active_service()` implementations. Fixing only one leaves the other broken.

## Service Unit File Pattern
Each model needs a separate systemd unit file at `/etc/systemd/system/llama-server-{short}.service`. Never share `llama-server.service` across models — it hardcodes one model's path.

## Port Audit: Actual Ports from Service Files (May 2026)
The ports in models.py MUST match what the systemd service files actually use. Verify by scanning:

```bash
# Extract actual ports from ALL service files
for f in /etc/systemd/system/llama-server*.service; do
  name=$(basename "$f" .service)
  port=$(grep -oP -- '--port \K\d+' "$f" 2>/dev/null || echo "?")
  model=$(grep -oP -- '-m /\S+/\K[^/\s]+\.gguf' "$f" 2>/dev/null || echo "?")
  echo "$name: $model (port $port)"
done
```

Correct mapping as of May 2026:

| Short | Model Name | Port | Service File | Context |
|-------|-----------|------|-------------|---------|
| 35b | Qwen3.6-35B-A3B-UD | 8081 | llama-server | 128000 |
| 14b | Qwen3-14B | 8088 | llama-server-qwen14b | 65536 |
| 8b | Qwen3-8B | 8086 | llama-server-qwen8b | 65536 |
| 4b | Qwen3-4B-Instruct | 8092 | llama-server-qwen4b | 131072 |
| 9b | Qwen3.5-9B-UD | 8082 | llama-server-9b | 128000 |
| deepseek | DeepSeek-R1-8B | 8083 | llama-server-ds-r1-8b | 32768 |
| gemma3 | Gemma-3-12B | 8089 | llama-server-gemma3 | 131072 |
| gemma3qat | Gemma-3-12B-QAT | 8091 | llama-server-gemma3-qat | 131072 |
| gemma4 | Gemma-4-26B | 8090 | llama-server-gemma4 | 131072 |
| smol | SmolLM3-3B-CPU | 8087 | llama-server-smol | 16384 |
| min8b | Ministral-3-8B | 8084 | llama-server-ministral-8b | 128000 |

## Port Conflict Mitigation
If two service files use the same port (e.g., both `llama-server-ministral.service` and `llama-server-gemma4.service` on 8090), resolve by switching one to a pre-existing alternative unit (e.g., `llama-server-ministral-8b.service` on port 8084) or by editing the conflicting unit's `ExecStart --port` flag. Always verify with `ss -tlnp | grep 8090` after starting.

## Model Inventory Cross-Reference (May 2026)
```
/models/llm/ — 11 GGUF models:
  daily/Qwen3.6-35B-A3B-UD-Q4_K_S.gguf          port 8081  (llama-server)
  qwen14b/qwen3-14b-Q4_K_M.gguf                  port 8088  (llama-server-qwen14b)
  qwen8b/qwen3-8b-Q4_K_M.gguf                     port 8086  (llama-server-qwen8b)
  qwen4b/Qwen3-4B-Instruct-2507-UD-Q8_K_XL.gguf  port 8092  (llama-server-qwen4b)
  daily/qwen3.5-9b-uncensored-Q4_K_M.gguf         port 8082  (llama-server-9b)
  daily/DeepSeek-R1-Distill-Llama-8B-Q5_K_M.gguf  port 8083  (llama-server-ds-r1-8b)
  gemma3/gemma-3-12b-Q4_K_M.gguf                  port 8089  (llama-server-gemma3)
  gemma3-qat/gemma-3-12b-it-qat-Q4_0.gguf         port 8091  (llama-server-gemma3-qat)
  gemma4/gemma-4-26B-IQ4_XS.gguf                  port 8090  (llama-server-gemma4)
  memory/HuggingFaceTB_SmolLM3-3B-Q4_K_M.gguf     port 8087  (llama-server-smol, preserved)
  ministral-8b/Ministral-3-8B-Reasoning-2512-Q5_K_M.gguf port 8084 (llama-server-ministral-8b)
```
