Auto-réglage GPU : détection, num_ctx optimal et bouton Réglage auto

Backend :
- agent_config : num_ctx dans la config, envoyé à Ollama (0 = défaut modèle)
- ollama_client : méthode ps() (placement GPU/CPU via /api/ps)
- autotune : placement() lit size vs size_vram pour savoir si le modèle est sur GPU
- routes/config : POST /api/config/auto (détecte GPU + modèle, calcule et applique
  num_ctx/max_tokens, renvoie la détection et le placement)

Frontend :
- client/store : autoTune(), état tuning + résultat
- SettingsView : bouton ⚡ Réglage auto, bannière de détection (GPU/VRAM,
  contexte, placement GPU/CPU), slider Contexte (num_ctx)

Config :
- GPU_VRAM_MB / GPU_NAME pour déclarer la VRAM si Ollama est distant

Tests : recommandation (8B sur 12 Go -> ctx 32768), route /auto + persistance,
transmission num_ctx aux options Ollama, placement via /api/ps.

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-06-30 05:54:30 +00:00
1 parent 98d7bb03fd
commit cbe9bcd514
10 files changed
+231 -5

No files matched your search

+7 -1
View File
@@ -34,6 +34,8 @@ DEFAULT_CONFIG: dict = {
"top_p": 0.9,
"top_k": 40,
"max_tokens": 2048,
# Fenêtre de contexte envoyée à Ollama (0 = laisser le défaut du modèle).
"num_ctx": 0,
"tools": dict(DEFAULT_TOOL_STATE),
# Demander une validation utilisateur avant toute commande shell.
"confirm_shell": True,
@@ -69,12 +71,16 @@ def save_config(patch: dict) -> dict:
def ollama_options(cfg: dict) -> dict:
"""Traduit la config en options de génération Ollama."""
return {
opts = {
"temperature": cfg["temperature"],
"top_p": cfg["top_p"],
"top_k": cfg["top_k"],
"num_predict": cfg["max_tokens"],
}
# num_ctx n'est envoyé que s'il est défini (> 0), sinon défaut du modèle.
if cfg.get("num_ctx"):
opts["num_ctx"] = cfg["num_ctx"]
return opts
def enabled_tool_names(cfg: dict) -> list[str]:
+25
View File
@@ -210,6 +210,31 @@ async def recommend(model: str) -> dict:
}
async def placement(model: str) -> dict:
"""Lit /api/ps : le modèle est-il chargé sur GPU, CPU, ou un mix ?"""
try:
loaded = await ollama.ps()
except (httpx.HTTPError, OSError):
return {"loaded": False}
for m in loaded:
if m.get("name") == model or m.get("model") == model:
size = m.get("size", 0) or 0
size_vram = m.get("size_vram", 0) or 0
if size <= 0:
where = "inconnu"
elif size_vram >= size * 0.99:
where = "gpu"
elif size_vram <= size * 0.01:
where = "cpu"
else:
where = "mixte"
pct = int(size_vram / size * 100) if size else 0
return {"loaded": True, "where": where, "gpu_percent": pct,
"size_mb": size // (1024 * 1024)}
return {"loaded": False}
def _fallback_model_mb(prof: dict) -> int:
"""Estime la taille des poids si /api/tags n'a rien donné."""
ps = (prof.get("parameter_size") or "").upper().replace("B", "")
+7
View File
@@ -33,6 +33,13 @@ class OllamaClient:
resp.raise_for_status()
return resp.json().get("models", [])
async def ps(self) -> list[dict]:
"""Modèles actuellement chargés et leur répartition VRAM/CPU (/api/ps)."""
async with httpx.AsyncClient(timeout=5.0, follow_redirects=True) as client:
resp = await client.get(f"{self.host}/api/ps")
resp.raise_for_status()
return resp.json().get("models", [])
async def show(self, name: str) -> dict:
"""Métadonnées détaillées d'un modèle (/api/show)."""
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client:
+34 -2
View File
@@ -1,10 +1,11 @@
"""Routes de configuration de l'agent (lecture / mise à jour)."""
"""Routes de configuration de l'agent (lecture / mise à jour / auto-réglage)."""
from __future__ import annotations
from fastapi import APIRouter
from pydantic import BaseModel
from .. import agent_config
from .. import agent_config, autotune
from ..config import settings
router = APIRouter(prefix="/api/config", tags=["config"])
@@ -15,7 +16,14 @@ class ConfigPatch(BaseModel):
top_p: float | None = None
top_k: int | None = None
max_tokens: int | None = None
num_ctx: int | None = None
tools: dict[str, bool] | None = None
confirm_shell: bool | None = None
class AutoTuneRequest(BaseModel):
model: str
apply: bool = True
@router.get("")
@@ -30,3 +38,27 @@ async def get_config() -> dict:
async def put_config(patch: ConfigPatch) -> dict:
cfg = agent_config.save_config(patch.model_dump(exclude_none=True))
return {"config": cfg}
@router.post("/auto")
async def auto_tune(req: AutoTuneRequest) -> dict:
"""Détecte le GPU + le modèle, calcule les réglages optimaux, et (par
défaut) les applique. Renvoie le détail de la détection pour l'UI."""
reco = await autotune.recommend(req.model)
placement = await autotune.placement(req.model)
applied = None
if req.apply:
applied = agent_config.save_config(
{
"num_ctx": reco["recommended"]["num_ctx"],
"max_tokens": reco["recommended"]["max_tokens"],
}
)
return {
"detection": reco,
"placement": placement,
"config": applied or agent_config.get_config(),
"vram_override": settings.gpu_vram_mb,
}