Files
tatlock/scripts/ollama_residency.py
jpmschweitzerandClaude bf13f9f0de refactor(bench): share the GPU residency guard, and guard tool calling too
Extracts the residency snapshot/restore into scripts/ollama_residency.py
so the two benchmarks cannot drift, and applies it to
benchmark_tool_calling.py, which had no protection at all.

That script was the more dangerous of the two. It rewrites
OLLAMA_DEFAULT_MODEL in .env and lets uvicorn reload onto it, restoring
the original only after the loop — so any crash or interrupt left the
*running server* pointed at the benchmark model. Its DEFAULT_MODELS
begins with mistral-nemo-large, the 9.2G model implicated in the
2026-08-07 VRAM outage. Both the .env restore and the residency restore
now run from `finally`.

SIGTERM is handled explicitly in the shared module. Python runs `finally`
for SIGINT, which arrives as KeyboardInterrupt, but the default SIGTERM
action terminates outright, so `timeout` or a plain `kill` skipped the
guard entirely.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 17:03:53 +02:00

122 lines
4.5 KiB
Python

"""
Guard production's GPU residency across a benchmark run.
Benchmarks swap models on the card production is serving from. Ollama evicts to
make room, so a run leaves its own models resident and the production one gone:
the next voice turn pays a ~36s cold load, and the pin that prevented it is
silently lost. That happened on 2026-08-08 — a routing benchmark evicted
gemma4:e2b and left gemma4:e4b behind, and only the monitoring noticing
`unexpected_models` caught it.
Snapshot before, restore after, and wire the restore to SIGTERM as well as the
normal path. Python runs `finally` for SIGINT, which arrives as
KeyboardInterrupt, but the default SIGTERM action terminates outright — so
`timeout`, a systemd stop or a plain `kill` would skip the guard entirely.
from scripts.ollama_residency import residency_guard, install_sigterm_handler
install_sigterm_handler()
with residency_guard(models_used=["gemma4:e4b"]):
...
"""
from __future__ import annotations
import signal
from collections.abc import Iterator
from contextlib import contextmanager
from datetime import UTC, datetime
from typing import Any
import httpx
OLLAMA_URL = "http://localhost:11434"
# keep_alive:-1 yields a year-2318 expiry, so "pinned" is simply "expires more
# than a day out". Matches check-ai-pipeline.sh in system-admin-toj.
PINNED_THRESHOLD_SECONDS = 86400
def install_sigterm_handler() -> None:
"""Make SIGTERM raise, so `finally` blocks and context managers still run."""
def _raise(signum, _frame):
raise KeyboardInterrupt(f"signal {signum}")
signal.signal(signal.SIGTERM, _raise)
def snapshot_residency(client: httpx.Client | None = None) -> dict[str, bool]:
"""Resident models mapped to whether each is pinned."""
owns = client is None
client = client or httpx.Client(timeout=30)
try:
data = client.get(f"{OLLAMA_URL}/api/ps", timeout=10).json()
except Exception: # noqa: BLE001 - a missing snapshot must not abort the run
return {}
finally:
if owns:
client.close()
resident: dict[str, bool] = {}
now = datetime.now(UTC)
for model in data.get("models", []):
pinned = False
try:
expires = datetime.fromisoformat(model.get("expires_at", "").replace("Z", "+00:00"))
pinned = (expires - now).total_seconds() > PINNED_THRESHOLD_SECONDS
except ValueError:
pass
resident[model["name"]] = pinned
return resident
def set_keep_alive(model: str, keep_alive: Any, client: httpx.Client | None = None) -> bool:
"""Load, unload or pin a model. Embedding models reject /api/generate."""
owns = client is None
client = client or httpx.Client(timeout=180)
payload = {"model": model, "keep_alive": keep_alive}
try:
for endpoint in ("generate", "embed"):
try:
response = client.post(f"{OLLAMA_URL}/api/{endpoint}", json=payload, timeout=180)
except Exception: # noqa: BLE001
return False
if response.status_code == 200:
return True
if response.status_code == 400 and "does not support generate" in response.text:
continue # embedding-only model; try /api/embed
return False
return False
finally:
if owns:
client.close()
def restore_residency(before: dict[str, bool], used: list[str]) -> None:
"""Evict what the benchmark loaded, then re-pin what was pinned before."""
base = {name.split(":")[0] for name in before}
with httpx.Client(timeout=180) as client:
for model in used:
if model not in before and model.split(":")[0] not in base:
print(f" residency: unloading benchmark model {model}")
set_keep_alive(model, 0, client)
for name, pinned in before.items():
if not pinned:
continue
ok = set_keep_alive(name, -1, client)
print(f" residency: re-pinned {name}" if ok
else f" residency: FAILED to re-pin {name} -- run warmup-ollama.sh")
@contextmanager
def residency_guard(models_used: list[str]) -> Iterator[dict[str, bool]]:
"""Snapshot residency on entry, restore it on exit however that happens."""
before = snapshot_residency()
pinned = [n for n, p in before.items() if p]
print(f" residency: resident before {sorted(before)}"
f"{f' (pinned: {pinned})' if pinned else ''}")
try:
yield before
finally:
print(" residency: restoring ...")
restore_residency(before, models_used)