mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
Fix chargement modèles : préchargement avec les mêmes options + alerte VRAM
Recherche : un modèle plus gros que la VRAM (ex. qwen3.6:35b-a3b ~20 Go sur 16 Go) déborde sur le CPU et charge lentement ; et le préchargement chargeait un runner aux options par défaut, aussitôt rechargé par le 1er message avec un num_ctx différent — double chargement d'un gros modèle. - ollama_client.warm : accepte et transmet les options de génération. - routes/models : start_model_warm calcule les options depuis la config du modèle (ollama_options) et les passe au warm -> runner identique au chat, plus de rechargement. Après chargement, détecte le placement via /api/ps (gpu/cpu/mixte + %) et l'expose dans l'état de warm. - Frontend : warmModel renvoie l'état ; si le modèle se charge sur CPU/mixte, message d'alerte clair (trop gros pour la VRAM, choisir plus petit/quant). Tests : options transmises au warm, placement 'mixte 65%' détecté, builds OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
4 files changed
+64
-12
No files matched your search
@@ -118,19 +118,27 @@ class OllamaClient:
|
|||||||
raise OllamaError(str(chunk["error"]))
|
raise OllamaError(str(chunk["error"]))
|
||||||
yield chunk
|
yield chunk
|
||||||
|
|
||||||
async def warm(self, model: str, keep_alive: str = "30m") -> dict:
|
async def warm(
|
||||||
|
self, model: str, keep_alive: str = "30m", options: dict | None = None
|
||||||
|
) -> dict:
|
||||||
"""Précharge un modèle en VRAM sans générer (/api/generate sans prompt).
|
"""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.
|
Le paramètre keep_alive fixe la durée de rétention en mémoire. Les
|
||||||
|
`options` (num_ctx, num_batch, num_gpu…) DOIVENT correspondre à celles du
|
||||||
|
chat : sinon Ollama chargerait un runner distinct puis en rechargerait un
|
||||||
|
autre au premier message — un double chargement très coûteux pour un gros
|
||||||
|
modèle.
|
||||||
"""
|
"""
|
||||||
|
payload: dict = {"model": model, "keep_alive": keep_alive, "stream": False}
|
||||||
|
if options:
|
||||||
|
payload["options"] = options
|
||||||
# Le chargement se fait désormais en tâche de fond côté API Loki. On lui
|
# Le chargement se fait désormais en tâche de fond côté API Loki. On lui
|
||||||
# laisse jusqu'à dix minutes pour les gros modèles ou un stockage lent.
|
# laisse jusqu'à dix minutes pour les gros modèles ou un stockage lent.
|
||||||
timeout = httpx.Timeout(connect=10.0, read=600.0, write=30.0, pool=10.0)
|
timeout = httpx.Timeout(connect=10.0, read=600.0, write=30.0, pool=10.0)
|
||||||
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
|
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
|
||||||
for attempt in range(2):
|
for attempt in range(2):
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
f"{self.host}/api/generate",
|
f"{self.host}/api/generate", json=payload
|
||||||
json={"model": model, "keep_alive": keep_alive, "stream": False},
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
# Inclut le corps JSON d'Ollama dans l'erreur (OOM, runner…),
|
# Inclut le corps JSON d'Ollama dans l'erreur (OOM, runner…),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from fastapi import APIRouter, HTTPException, Query
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .. import agent_config
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from ..ollama_client import ollama
|
from ..ollama_client import ollama
|
||||||
|
|
||||||
@@ -70,10 +71,29 @@ class WarmRequest(BaseModel):
|
|||||||
keep_alive: str = "30m"
|
keep_alive: str = "30m"
|
||||||
|
|
||||||
|
|
||||||
async def _warm_in_background(name: str, keep_alive: str) -> None:
|
async def _placement_of(name: str) -> dict:
|
||||||
|
"""Où le modèle vient-il d'être chargé ? (GPU / CPU / mixte), via /api/ps."""
|
||||||
try:
|
try:
|
||||||
await ollama.warm(name, keep_alive)
|
for m in await ollama.ps():
|
||||||
_warm_states[name] = {"state": "loaded"}
|
if m.get("name") == name or m.get("model") == name:
|
||||||
|
size = m.get("size", 0) or 0
|
||||||
|
vram = m.get("size_vram", 0) or 0
|
||||||
|
if size <= 0:
|
||||||
|
return {}
|
||||||
|
pct = int(vram / size * 100)
|
||||||
|
where = "gpu" if pct >= 99 else "cpu" if pct <= 1 else "mixte"
|
||||||
|
return {"processor": where, "gpu_percent": str(pct)}
|
||||||
|
except (httpx.HTTPError, OSError):
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
async def _warm_in_background(name: str, keep_alive: str, options: dict | None) -> None:
|
||||||
|
try:
|
||||||
|
await ollama.warm(name, keep_alive, options)
|
||||||
|
state = {"state": "loaded"}
|
||||||
|
state.update(await _placement_of(name))
|
||||||
|
_warm_states[name] = state
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_warm_states[name] = {
|
_warm_states[name] = {
|
||||||
"state": "error",
|
"state": "error",
|
||||||
@@ -82,12 +102,19 @@ async def _warm_in_background(name: str, keep_alive: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def start_model_warm(name: str, keep_alive: str) -> None:
|
def start_model_warm(name: str, keep_alive: str) -> None:
|
||||||
"""Démarre au plus une tâche de préchargement par modèle."""
|
"""Démarre au plus une tâche de préchargement par modèle.
|
||||||
|
|
||||||
|
Les options de génération du modèle (num_ctx, num_batch, num_gpu…) sont
|
||||||
|
envoyées au préchargement pour qu'Ollama charge exactement le runner que le
|
||||||
|
chat utilisera — évite un rechargement complet au premier message.
|
||||||
|
"""
|
||||||
current = _warm_tasks.get(name)
|
current = _warm_tasks.get(name)
|
||||||
if current and not current.done():
|
if current and not current.done():
|
||||||
return
|
return
|
||||||
|
cfg = agent_config.get_config(name)
|
||||||
|
options = agent_config.ollama_options(cfg)
|
||||||
_warm_states[name] = {"state": "loading"}
|
_warm_states[name] = {"state": "loading"}
|
||||||
task = asyncio.create_task(_warm_in_background(name, keep_alive))
|
task = asyncio.create_task(_warm_in_background(name, keep_alive, options))
|
||||||
_warm_tasks[name] = task
|
_warm_tasks[name] = task
|
||||||
|
|
||||||
def forget(done: asyncio.Task[None]) -> None:
|
def forget(done: asyncio.Task[None]) -> None:
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ async function apiError(res: Response, fallback: string): Promise<Error> {
|
|||||||
interface WarmStatus {
|
interface WarmStatus {
|
||||||
state: "idle" | "loading" | "loaded" | "error";
|
state: "idle" | "loading" | "loaded" | "error";
|
||||||
error?: string;
|
error?: string;
|
||||||
|
processor?: "gpu" | "cpu" | "mixte";
|
||||||
|
gpu_percent?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
@@ -107,7 +109,10 @@ const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|||||||
* Lance le préchargement en arrière-plan puis suit son état. Chaque requête
|
* Lance le préchargement en arrière-plan puis suit son état. Chaque requête
|
||||||
* reste courte afin qu'un reverse proxy ne puisse plus interrompre le warm-up.
|
* reste courte afin qu'un reverse proxy ne puisse plus interrompre le warm-up.
|
||||||
*/
|
*/
|
||||||
export async function warmModel(name: string, keepAlive = "30m"): Promise<void> {
|
export async function warmModel(
|
||||||
|
name: string,
|
||||||
|
keepAlive = "30m"
|
||||||
|
): Promise<WarmStatus> {
|
||||||
const res = await fetch("/api/models/warm", {
|
const res = await fetch("/api/models/warm", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -126,7 +131,7 @@ export async function warmModel(name: string, keepAlive = "30m"): Promise<void>
|
|||||||
throw await apiError(statusRes, `suivi du préchargement refusé (${statusRes.status})`);
|
throw await apiError(statusRes, `suivi du préchargement refusé (${statusRes.status})`);
|
||||||
}
|
}
|
||||||
const status = (await statusRes.json()) as WarmStatus;
|
const status = (await statusRes.json()) as WarmStatus;
|
||||||
if (status.state === "loaded") return;
|
if (status.state === "loaded") return status;
|
||||||
if (status.state === "error") {
|
if (status.state === "error") {
|
||||||
throw new Error(status.error ?? "préchargement impossible");
|
throw new Error(status.error ?? "préchargement impossible");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,8 +159,20 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
// La configuration est propre au modèle : on attend son chargement avant
|
// La configuration est propre au modèle : on attend son chargement avant
|
||||||
// d'utiliser keep_alive, sinon la valeur du modèle précédent est envoyée.
|
// d'utiliser keep_alive, sinon la valeur du modèle précédent est envoyée.
|
||||||
const ka = get().config?.keep_alive ?? "30m";
|
const ka = get().config?.keep_alive ?? "30m";
|
||||||
await warmModel(name, ka);
|
const st = await warmModel(name, ka);
|
||||||
await get().refreshLoadedModels();
|
await get().refreshLoadedModels();
|
||||||
|
// Modèle chargé hors GPU : prévenir (souvent trop gros pour la VRAM).
|
||||||
|
if (
|
||||||
|
(st.processor === "cpu" || st.processor === "mixte") &&
|
||||||
|
get().selectedModel === name
|
||||||
|
) {
|
||||||
|
set({
|
||||||
|
warmError:
|
||||||
|
st.processor === "cpu"
|
||||||
|
? "Modèle chargé sur le CPU (trop gros pour la VRAM) — lent. Choisis un modèle plus petit ou une quantization plus légère."
|
||||||
|
: `Modèle en partie sur GPU (${st.gpu_percent ?? "?"}%) : dépasse la VRAM, chargement plus lent.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (get().selectedModel === name) {
|
if (get().selectedModel === name) {
|
||||||
set({
|
set({
|
||||||
|
|||||||
Reference in new issue
Block a user