Nomme la cause d'un débit anormal : placement du modèle hors GPU

Un modèle dont les couches partent en RAM tombe à quelques jetons par
seconde. Loki subissait la lenteur sans jamais la nommer : l'utilisateur
ne pouvait pas distinguer un blocage de l'application d'un modèle placé
sur le CPU par Ollama.

Au début de chaque réponse, si /api/ps indique que le modèle n'est pas
entièrement en VRAM, une notice donne le pourcentage réellement en VRAM
et le volume resté en RAM.

Le placement appartient à Ollama — Loki le constate, il ne le corrige
pas. Silencieux quand tout est sur GPU, quand le modèle n'est pas encore
chargé, ou quand Ollama ne répond pas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
Claude committed 2026-08-03 11:50:45 +00:00
1 parent b3835e7977
commit b3914e438c
1 file changed
+39
+39
View File
@@ -17,6 +17,7 @@ import re
import time import time
from contextlib import suppress from contextlib import suppress
import httpx
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from pydantic import BaseModel from pydantic import BaseModel
@@ -28,6 +29,7 @@ from .. import router as msg_router
from ..tools import check_html, _safe_path from ..tools import check_html, _safe_path
from ..agent import run_agent from ..agent import run_agent
from ..config import settings from ..config import settings
from ..ollama_client import ollama
router = APIRouter(prefix="/api", tags=["chat"]) router = APIRouter(prefix="/api", tags=["chat"])
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -62,6 +64,37 @@ def _apply_mode(cfg: dict, mode: str) -> dict:
return cfg return cfg
async def _placement_notice(model: str) -> str | None:
"""Avertit si le modèle tourne (au moins en partie) hors du GPU.
C'est LA cause d'un débit ridicule (quelques jetons/s) : Ollama a placé
tout ou partie des couches en RAM. Loki ne peut pas le corriger — le
placement appartient à Ollama — mais il peut le NOMMER, au lieu de
laisser croire à un blocage de l'application.
"""
try:
loaded = await ollama.ps()
except (httpx.HTTPError, OSError):
return None
for m in loaded:
if (m.get("name") or m.get("model")) != model:
continue
size = m.get("size", 0) or 0
vram = m.get("size_vram", 0) or 0
if size <= 0 or vram >= size * 0.99:
return None
pct = int(vram / size * 100)
in_ram = (size - vram) / 1e9
where = "entièrement en RAM" if pct == 0 else f"{pct} % en VRAM"
return (
f"⚠️ {model} tourne {where} ({in_ram:.1f} Go hors GPU) : "
"le débit sera de quelques jetons par seconde. Réduis le contexte, "
"prends un modèle plus petit, ou répartis sur tes GPU côté Ollama."
)
return None
def _loading_message(model: str, seconds: float) -> str: def _loading_message(model: str, seconds: float) -> str:
"""Message d'attente avant le premier jeton (chargement du modèle).""" """Message d'attente avant le premier jeton (chargement du modèle)."""
waited = int(seconds) waited = int(seconds)
@@ -421,6 +454,12 @@ async def chat(req: ChatRequest) -> StreamingResponse:
for mcp_notice in mcp_manager.notices(): for mcp_notice in mcp_manager.notices():
yield _sse("notice", {"message": mcp_notice}) yield _sse("notice", {"message": mcp_notice})
# Placement du modèle : dire tout de suite si l'inférence est hors GPU,
# au lieu de laisser l'utilisateur attribuer la lenteur à Loki.
placement = await _placement_notice(model)
if placement:
yield _sse("notice", {"message": placement})
# ── Consignes additionnelles du tour ────────────────────────────── # ── Consignes additionnelles du tour ──────────────────────────────
# Elles sont COLLECTÉES ici puis fusionnées dans l'UNIQUE message # Elles sont COLLECTÉES ici puis fusionnées dans l'UNIQUE message
# système (voir _merge_system plus bas). Beaucoup de templates # système (voir _merge_system plus bas). Beaucoup de templates