From f174c8d58d7722cd2de5eff85785fe83ad3fdde6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 23:16:09 +0000 Subject: [PATCH] =?UTF-8?q?Panneau=20Mat=C3=A9riel=20:=20GPU=20vu=20par=20?= =?UTF-8?q?Loki=20vs=20GPU=20utilis=C3=A9=20par=20Ollama?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - routes/system : /api/system/hardware — liste les GPU vus par le conteneur (nvidia-smi, multi-GPU) ou l'override GPU_VRAM_MB, et le placement réel des modèles chargés côté Ollama (GPU/CPU/mixte + VRAM) via /api/ps. - config : réintègre gpu_vram_mb / gpu_name (perdus lors d'une fusion). - UI : carte MATÉRIEL dans Configuration (rafraîchie toutes les 5 s) montrant les deux points de vue et si Ollama est local ou distant. Répond à « l'appli est-elle réservée à la 3060 ? » (non) et permet de voir tout de suite si Ollama charge un modèle sur CPU faute de support GPU. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N --- backend/app/config.py | 5 ++ backend/app/routes/system.py | 91 +++++++++++++++++++++ frontend/src/api/client.ts | 31 +++++++ frontend/src/panels/SettingsView.tsx | 118 ++++++++++++++++++++++++++- 4 files changed, 243 insertions(+), 2 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index 3285bee..2314578 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -15,6 +15,11 @@ class Settings(BaseSettings): # Permet de vérifier que l'image déployée est bien à jour. loki_version: str = "dev" + # Override manuel de la VRAM (Mo) si la détection GPU échoue dans le + # conteneur (utile quand Ollama tourne sur une autre machine). + gpu_vram_mb: int = 0 + gpu_name: str = "" + model_config = SettingsConfigDict(env_file=".env", extra="ignore") diff --git a/backend/app/routes/system.py b/backend/app/routes/system.py index 24153cf..8a4d9fc 100644 --- a/backend/app/routes/system.py +++ b/backend/app/routes/system.py @@ -4,9 +4,13 @@ from __future__ import annotations import asyncio import shutil +import httpx import psutil from fastapi import APIRouter +from ..config import settings +from ..ollama_client import ollama + router = APIRouter(prefix="/api/system", tags=["system"]) _NVIDIA_SMI = shutil.which("nvidia-smi") @@ -50,3 +54,90 @@ async def stats() -> dict: "ram_pct": mem.percent, "gpu": gpu, } + + +async def _all_local_gpus() -> list[dict]: + """Tous les GPU NVIDIA visibles depuis le CONTENEUR Loki (peut être vide).""" + if not _NVIDIA_SMI: + return [] + try: + proc = await asyncio.create_subprocess_exec( + _NVIDIA_SMI, + "--query-gpu=index,name,memory.total,memory.used,utilization.gpu", + "--format=csv,noheader,nounits", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + out, _ = await asyncio.wait_for(proc.communicate(), timeout=3) + gpus = [] + for line in out.decode().strip().splitlines(): + idx, name, total, used, util = (p.strip() for p in line.split(",")) + gpus.append({ + "index": int(idx), "name": name, + "vram_total_mb": float(total), "vram_used_mb": float(used), + "util_pct": float(util), + }) + return gpus + except Exception: + return [] + + +@router.get("/hardware") +async def hardware() -> dict: + """Vue matériel : GPU vu par Loki vs GPU réellement utilisé par Ollama.""" + # 1) Ce que voit le conteneur Loki (nvidia-smi) + override éventuel. + local_gpus = await _all_local_gpus() + override = None + if settings.gpu_vram_mb > 0: + override = { + "name": settings.gpu_name or "GPU déclaré (GPU_VRAM_MB)", + "vram_total_mb": settings.gpu_vram_mb, + } + + # 2) Ce qu'Ollama utilise réellement (modèles chargés + placement VRAM/CPU). + ollama_info: dict = {"host": ollama.host, "connected": False, "running": []} + try: + version = await ollama.ping() + ollama_info["connected"] = True + ollama_info["version"] = version.get("version") + except (httpx.HTTPError, OSError) as exc: + ollama_info["error"] = str(exc)[:200] + + if ollama_info["connected"]: + try: + for m in await ollama.ps(): + size = m.get("size", 0) or 0 + vram = m.get("size_vram", 0) or 0 + if size <= 0: + where = "inconnu" + elif vram >= size * 0.99: + where = "GPU" + elif vram <= size * 0.01: + where = "CPU" + else: + where = "mixte" + ollama_info["running"].append({ + "name": m.get("name") or m.get("model"), + "processor": where, + "gpu_percent": int(vram / size * 100) if size else 0, + "size_mb": round(size / 1_000_000), + "vram_mb": round(vram / 1_000_000), + }) + except (httpx.HTTPError, OSError): + pass + + # Ollama est-il sur la même machine que Loki ? (heuristique sur l'hôte) + host = ollama.host.lower() + is_local = any(h in host for h in ("localhost", "127.0.0.1", "host.docker.internal")) + + return { + "loki_gpus": local_gpus, + "gpu_override": override, + "ollama": ollama_info, + "ollama_is_local": is_local, + "note": ( + "Loki ne voit pas directement le GPU d'un Ollama distant : il déduit " + "le placement (GPU/CPU) depuis les modèles chargés. Déclare GPU_VRAM_MB " + "pour l'auto-réglage si Ollama tourne sur une autre machine." + ), + } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index eaead58..8bca677 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -41,6 +41,37 @@ export async function getSystemStats(): Promise { return res.json(); } +export interface HardwareInfo { + loki_gpus: { + index: number; + name: string; + vram_total_mb: number; + vram_used_mb: number; + util_pct: number; + }[]; + gpu_override: { name: string; vram_total_mb: number } | null; + ollama: { + host: string; + connected: boolean; + version?: string; + error?: string; + running: { + name: string; + processor: string; + gpu_percent: number; + size_mb: number; + vram_mb: number; + }[]; + }; + ollama_is_local: boolean; + note: string; +} + +export async function getHardware(): Promise { + const res = await fetch("/api/system/hardware"); + return res.json(); +} + export async function getVersion(): Promise { try { const res = await fetch("/api/version"); diff --git a/frontend/src/panels/SettingsView.tsx b/frontend/src/panels/SettingsView.tsx index 585591f..0d458ba 100644 --- a/frontend/src/panels/SettingsView.tsx +++ b/frontend/src/panels/SettingsView.tsx @@ -1,7 +1,13 @@ import { useEffect, useState } from "react"; import { useStore } from "../store/useStore"; -import { deleteModel, getBenchScores, pullModel, runBench } from "../api/client"; -import type { AgentConfig, BenchResult } from "../api/client"; +import { + deleteModel, + getBenchScores, + getHardware, + pullModel, + runBench, +} from "../api/client"; +import type { AgentConfig, BenchResult, HardwareInfo } from "../api/client"; import { DownloadIcon, RefreshIcon } from "../components/Icon"; const TOOL_DESC: Record = { @@ -460,6 +466,8 @@ export function SettingsView() { + + @@ -489,6 +497,112 @@ export function SettingsView() { } /** Benchmark : évalue le modèle sélectionné sur 5 mini-épreuves. */ +/** Panneau Matériel : GPU vu par Loki vs GPU réellement utilisé par Ollama. */ +function HardwareCard() { + const [hw, setHw] = useState(null); + + const refresh = () => getHardware().then(setHw).catch(() => {}); + useEffect(() => { + refresh(); + const id = setInterval(refresh, 5000); + return () => clearInterval(id); + }, []); + + return ( + +
+
MATÉRIEL
+ +
+ + {/* GPU vu par le conteneur Loki */} +
+ GPU vu par Loki (conteneur) +
+ {hw?.loki_gpus.length ? ( + hw.loki_gpus.map((g) => ( +
+
+ {g.name} + + {(g.vram_used_mb / 1024).toFixed(1)}/ + {(g.vram_total_mb / 1024).toFixed(1)} Go · {g.util_pct.toFixed(0)}% + +
+
+ )) + ) : hw?.gpu_override ? ( +
+ {hw.gpu_override.name} ·{" "} + {(hw.gpu_override.vram_total_mb / 1024).toFixed(0)} Go{" "} + (déclaré) +
+ ) : ( +
+ Aucun GPU visible depuis le conteneur — normal si Ollama tourne sur une + autre machine. Déclare GPU_VRAM_MB pour l'auto-réglage. +
+ )} + + {/* GPU utilisé par Ollama */} +
+ Ollama {hw?.ollama_is_local ? "(local)" : "(distant)"} +
+
+
+ + + {hw?.ollama.host} + + {hw?.ollama.version && ( + v{hw.ollama.version} + )} +
+ {hw?.ollama.running.length ? ( + hw.ollama.running.map((m) => ( +
+ + {m.name} + + + {m.processor} + {m.processor === "mixte" ? ` ${m.gpu_percent}%` : ""} + + + {(m.vram_mb / 1024).toFixed(1)}G VRAM + +
+ )) + ) : ( +
+ Aucun modèle chargé actuellement. +
+ )} +
+ +
{hw?.note}
+
+ ); +} + function BenchCard() { const selectedModel = useStore((s) => s.selectedModel); const [scores, setScores] = useState>({});