Warm Ollama into VRAM on up and tunnel so Assistent chat is not cold.
A 1-token /api/chat after tags (and again if /api/ps is empty) loads VL weights before the first message. Mid KEEP_ALIVE is 15m so a short image-gen burst does not unload the model. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,10 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import yaml
|
||||
|
||||
from gpu_rent.paths import (
|
||||
@@ -138,6 +141,91 @@ def already_have_ollama_tag(have: set[str], wanted: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def preferred_ollama_model(path: Path) -> str | None:
|
||||
"""Manifest default, else first tag."""
|
||||
entries = parse_ollama_models(path)
|
||||
for entry in entries:
|
||||
if entry.default:
|
||||
return entry.name
|
||||
return entries[0].name if entries else None
|
||||
|
||||
|
||||
def _ollama_ps_names(payload: object) -> set[str]:
|
||||
names: set[str] = set()
|
||||
if not isinstance(payload, dict):
|
||||
return names
|
||||
for item in payload.get("models") or []:
|
||||
if isinstance(item, dict):
|
||||
for key in ("name", "model"):
|
||||
val = item.get(key)
|
||||
if val:
|
||||
names.add(str(val))
|
||||
elif isinstance(item, str) and item.strip():
|
||||
names.add(item.strip())
|
||||
return names
|
||||
|
||||
|
||||
def warmup_ollama_http(
|
||||
base_url: str,
|
||||
model: str,
|
||||
*,
|
||||
keep_alive: str = "15m",
|
||||
num_ctx: int = 16384,
|
||||
timeout: float = 180.0,
|
||||
) -> str:
|
||||
"""Load ``model`` into VRAM via a 1-token /api/chat. Skip if /api/ps already has it."""
|
||||
base = str(base_url).rstrip("/")
|
||||
t0 = time.monotonic()
|
||||
timeout_cfg = httpx.Timeout(timeout, connect=8.0)
|
||||
with httpx.Client(timeout=timeout_cfg) as client:
|
||||
try:
|
||||
ps = client.get(f"{base}/api/ps", timeout=8.0)
|
||||
ps.raise_for_status()
|
||||
if already_have_ollama_tag(_ollama_ps_names(ps.json()), model):
|
||||
return f"Ollama warmup skip — {model} уже в VRAM"
|
||||
except (httpx.HTTPError, ValueError, TypeError):
|
||||
pass
|
||||
body = {
|
||||
"model": model,
|
||||
"stream": False,
|
||||
"keep_alive": keep_alive,
|
||||
"options": {
|
||||
"num_predict": 1,
|
||||
"num_ctx": int(num_ctx),
|
||||
"temperature": 0,
|
||||
},
|
||||
"messages": [{"role": "user", "content": "ok"}],
|
||||
}
|
||||
resp = client.post(f"{base}/api/chat", json=body)
|
||||
resp.raise_for_status()
|
||||
elapsed = time.monotonic() - t0
|
||||
return f"Ollama warmup {model} ok ({elapsed:.1f}s)"
|
||||
|
||||
|
||||
def maybe_warmup_ollama_local(cfg: Any, log: Callable[[str], None]) -> None:
|
||||
"""After the laptop tunnel is up: load preferred tag if VRAM is cold."""
|
||||
try:
|
||||
if normalize_runtime(getattr(cfg, "llm_runtime", "none")) != "ollama":
|
||||
return
|
||||
except ValueError:
|
||||
return
|
||||
manifest = Path(getattr(cfg, "ollama_models_manifest", "") or "")
|
||||
model = preferred_ollama_model(manifest)
|
||||
if not model:
|
||||
return
|
||||
port = int(getattr(cfg, "ollama_local_port", 17811) or 17811)
|
||||
log(f"Ollama warmup {model} (VRAM)")
|
||||
try:
|
||||
log(
|
||||
warmup_ollama_http(
|
||||
f"http://127.0.0.1:{port}",
|
||||
model,
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
log(f"⚠ Ollama warmup: {exc}")
|
||||
|
||||
|
||||
def write_ollama_models_preset(path: Path, preset: str) -> None:
|
||||
key = (preset or "recommended").strip().lower()
|
||||
if key not in OLLAMA_PRESETS:
|
||||
|
||||
Reference in New Issue
Block a user