mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-19 13:08:15 +02:00
Préchargement des modèles : warm + keep_alive + indicateur d'état
Évite le rechargement lent quand Ollama a déchargé le modèle de la VRAM : - ollama_client.warm() : précharge un modèle (/api/generate sans prompt) ; chat() accepte keep_alive (durée de rétention en VRAM). - agent.run_agent transmet keep_alive ; route chat le passe depuis la config. - config : champ keep_alive (défaut 30m) par profil de modèle. - routes/models : POST /api/models/warm, GET /api/models/loaded (placement GPU/CPU via /api/ps). - main : préchargement du modèle par défaut au démarrage (arrière-plan, best-effort — n'empêche pas le démarrage si Ollama est absent). - Frontend : préchargement automatique à la sélection d'un modèle, poll des modèles chargés (8s), pastille verte (GPU) / orange (CPU) / blanche (à charger) dans le sélecteur, réglage 'Maintien en VRAM' dans Configuration. Tests : warm/loaded routes, keep_alive transmis, démarrage résilient sans Ollama, build front. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
12 files changed
+205
-8
No files matched your search
@@ -80,6 +80,7 @@ async def run_agent(
|
||||
enabled_tools: list[str] | None = None,
|
||||
confirm_shell: bool = True,
|
||||
think: bool = True,
|
||||
keep_alive: str | None = None,
|
||||
) -> AsyncIterator[dict]:
|
||||
# enabled_tools=None -> tous les outils ; liste vide -> aucun outil.
|
||||
if enabled_tools is None:
|
||||
@@ -127,6 +128,7 @@ async def run_agent(
|
||||
tools=active_tools,
|
||||
options=request_options,
|
||||
think=request_think,
|
||||
keep_alive=keep_alive,
|
||||
stream=True,
|
||||
):
|
||||
msg = chunk.get("message", {})
|
||||
@@ -314,7 +316,7 @@ async def run_agent(
|
||||
final_chunk = ""
|
||||
async for chunk in ollama.chat(
|
||||
model, convo, options=request_options,
|
||||
think=request_think, stream=True
|
||||
think=request_think, keep_alive=keep_alive, stream=True
|
||||
):
|
||||
tok = chunk.get("message", {}).get("content", "")
|
||||
if tok:
|
||||
|
||||
@@ -64,6 +64,7 @@ PROFILE_FIELDS = {
|
||||
"self_review",
|
||||
"rag_enabled",
|
||||
"embed_model",
|
||||
"keep_alive",
|
||||
*GENERATION_FIELDS,
|
||||
}
|
||||
|
||||
@@ -106,6 +107,9 @@ DEFAULT_CONFIG: dict = {
|
||||
# Mémoire long-terme (RAG) entre sessions, via un modèle d'embedding.
|
||||
"rag_enabled": True,
|
||||
"embed_model": "auto",
|
||||
# Durée de maintien du modèle en VRAM (préchargement). "0" = décharge
|
||||
# aussitôt, "-1" = jamais, "30m" = 30 minutes.
|
||||
"keep_alive": "30m",
|
||||
}
|
||||
|
||||
|
||||
|
||||
+25
-1
@@ -12,17 +12,41 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import coder, db, rag
|
||||
from . import agent_config, coder, db, rag
|
||||
from .config import settings
|
||||
from .routes import benchmark, chat, config, files, models, sessions, shell, system
|
||||
|
||||
|
||||
async def _warm_default_model() -> None:
|
||||
"""Précharge le modèle par défaut en VRAM au démarrage (best-effort)."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from .ollama_client import OllamaError, ollama
|
||||
|
||||
await asyncio.sleep(2) # laisse le service démarrer
|
||||
try:
|
||||
cfg = agent_config.get_config(settings.default_model)
|
||||
await ollama.warm(settings.default_model, cfg.get("keep_alive", "30m"))
|
||||
logging.getLogger(__name__).info(
|
||||
"Modèle %s préchargé en VRAM", settings.default_model
|
||||
)
|
||||
except (OllamaError, OSError, Exception) as exc: # best-effort
|
||||
logging.getLogger(__name__).info(
|
||||
"Préchargement au démarrage ignoré : %s", exc
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
import asyncio
|
||||
|
||||
db.init_db()
|
||||
rag.init_table()
|
||||
# Workspace en dépôt git : requis pour les commits du moteur code (Aider).
|
||||
coder.ensure_git(settings.workspace_dir)
|
||||
# Préchargement du modèle par défaut, sans bloquer le démarrage.
|
||||
asyncio.create_task(_warm_default_model())
|
||||
yield
|
||||
|
||||
|
||||
|
||||
@@ -117,6 +117,19 @@ class OllamaClient:
|
||||
raise OllamaError(str(chunk["error"]))
|
||||
yield chunk
|
||||
|
||||
async def warm(self, model: str, keep_alive: str = "30m") -> dict:
|
||||
"""Précharge un modèle en VRAM sans générer (/api/generate sans prompt).
|
||||
|
||||
Le paramètre keep_alive fixe la durée de rétention en mémoire.
|
||||
"""
|
||||
async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client:
|
||||
resp = await client.post(
|
||||
f"{self.host}/api/generate",
|
||||
json={"model": model, "keep_alive": keep_alive},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
model: str,
|
||||
@@ -125,6 +138,7 @@ class OllamaClient:
|
||||
tools: list[dict] | None = None,
|
||||
options: dict | None = None,
|
||||
think: bool | None = None,
|
||||
keep_alive: str | None = None,
|
||||
stream: bool = True,
|
||||
) -> AsyncIterator[dict]:
|
||||
"""Conversation avec le modèle, en streaming token par token."""
|
||||
@@ -136,6 +150,9 @@ class OllamaClient:
|
||||
# think=False désactive le raisonnement des modèles « thinking ».
|
||||
if think is not None:
|
||||
payload["think"] = think
|
||||
# keep_alive : durée de maintien du modèle en VRAM après la réponse.
|
||||
if keep_alive is not None:
|
||||
payload["keep_alive"] = keep_alive
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
timeout=_STREAM_TIMEOUT, follow_redirects=True
|
||||
|
||||
@@ -245,6 +245,7 @@ async def chat(req: ChatRequest) -> StreamingResponse:
|
||||
enabled_tools=agent_config.enabled_tool_names(cfg),
|
||||
confirm_shell=cfg.get("confirm_shell", True),
|
||||
think=cfg.get("think", True),
|
||||
keep_alive=cfg.get("keep_alive", "30m"),
|
||||
):
|
||||
await queue.put(event)
|
||||
except Exception as exc:
|
||||
|
||||
@@ -58,6 +58,42 @@ async def list_models() -> dict:
|
||||
return {"models": models, "default": settings.default_model}
|
||||
|
||||
|
||||
class WarmRequest(BaseModel):
|
||||
name: str
|
||||
keep_alive: str = "30m"
|
||||
|
||||
|
||||
@router.post("/models/warm")
|
||||
async def warm_model(req: WarmRequest) -> dict:
|
||||
"""Précharge un modèle en VRAM (préchargement)."""
|
||||
if not req.name.strip():
|
||||
raise HTTPException(400, "nom de modèle vide")
|
||||
try:
|
||||
await ollama.warm(req.name.strip(), req.keep_alive)
|
||||
except (httpx.HTTPError, OSError) as exc:
|
||||
raise HTTPException(502, f"préchargement impossible : {exc}") from exc
|
||||
return {"warmed": req.name.strip()}
|
||||
|
||||
|
||||
@router.get("/models/loaded")
|
||||
async def loaded_models() -> dict:
|
||||
"""Modèles actuellement chargés en mémoire + placement GPU/CPU (/api/ps)."""
|
||||
try:
|
||||
loaded = await ollama.ps()
|
||||
except (httpx.HTTPError, OSError):
|
||||
return {"loaded": []}
|
||||
result = []
|
||||
for m in loaded:
|
||||
size = m.get("size", 0) or 0
|
||||
vram = m.get("size_vram", 0) or 0
|
||||
result.append({
|
||||
"name": m.get("name") or m.get("model"),
|
||||
"on_gpu": bool(size and vram >= size * 0.99),
|
||||
"gpu_percent": int(vram / size * 100) if size else 0,
|
||||
})
|
||||
return {"loaded": result}
|
||||
|
||||
|
||||
class PullRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
Reference in new issue
Block a user