From 654720ed0549cbbca39822e58151abdaffcc0a97 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sun, 21 Jun 2026 13:13:30 +0200 Subject: [PATCH] v4 Schritt 1: optimale Kontextfenster automatisch ermitteln - hw_math: max_ctx_for() (Umkehrung der ctx-Heuristik) + extract_params_b + recommend_ctx. - cookbook.py: optimal_ctx pro Datei in analyze + evaluate. - models.py status: params_b + optimal_ctx pro Modell (psutil-RAM). - models.js Konfig-Modal: Empfehlung + 'Optimal uebernehmen'. - cookbook.js Modal: 'Empfohlener Kontext ... uebernehmen'. Co-Authored-By: Claude Opus 4.8 --- hw_math.py | 43 ++++++++++++++++++++++++++++++++++++ routers/cookbook.py | 4 +++- routers/models.py | 10 ++++++++- static/js/panels/cookbook.js | 8 ++++++- static/js/panels/models.js | 13 +++++++++++ 5 files changed, 75 insertions(+), 3 deletions(-) diff --git a/hw_math.py b/hw_math.py index 6d1704b..6f5985f 100644 --- a/hw_math.py +++ b/hw_math.py @@ -3,6 +3,8 @@ Extrahierte Mathematik aus dem Odysseus Projekt zur VRAM/RAM Berechnung. Abgestimmt auf APUs mit Unified Memory (Bosgame M5 / Strix Halo). """ +import re + # Annahme: Bytes per Parameter für GGUF Quants QUANT_BYTES_PER_PARAM = { "Q2_K": 0.35, @@ -69,3 +71,44 @@ def evaluate_fit(params_b: float, quant: str, ctx: int, sys_ram_gb: float) -> di "req_gb": round(req_gb, 1), "tps": round(tps, 0) } + + +def extract_params_b(name: str) -> float: + """Parametergröße (Mrd.) aus Repo-/Dateiname. 8x7B (MoE) -> 56.""" + moe = re.search(r"(\d+)x(\d+(?:\.\d+)?)[bB]", name) + if moe: + return float(moe.group(1)) * float(moe.group(2)) + m = re.search(r"(\d+(?:\.\d+)?)[bB](?![a-zA-Z])", name) + if m: + return float(m.group(1)) + return 7.0 + + +# "Schöne" Kontextstufen, die UIs/Modelle gern mögen. +_NICE_CTX = [2048, 4096, 8192, 16384, 32768, 49152, 65536, 98304, 131072] + + +def max_ctx_for(params_b: float, quant: str, sys_ram_gb: float) -> int: + """Größter 'schöner' Kontext, der komfortabel passt (80% des nutzbaren RAM, 4 GB OS-Puffer). + Umkehrung von estimate_memory_gb: löst die Kontext-Heuristik nach ctx auf.""" + bpp = QUANT_BYTES_PER_PARAM.get(quant.upper(), 0.65) + weights = params_b * bpp + usable = max(sys_ram_gb - 4.0, 0) * 0.8 # gleicher Komfort wie evaluate_fit "perfect" + ctx_budget = usable - weights # GB, die für den KV-Cache übrig sind + if ctx_budget <= 0: + return 2048 # Modell selbst schon knapp -> Minimal-Kontext + per_8k = (max(params_b, 7) / 7) * 0.8 # GB pro 8k Kontext (aus der Heuristik) + raw_ctx = (ctx_budget / per_8k) * 8192 + best = _NICE_CTX[0] + for c in _NICE_CTX: + if c <= raw_ctx: + best = c + return best + + +def recommend_ctx(params_b: float, quant: str, sys_ram_gb: float) -> dict: + """Empfohlener Kontext + Klartext-Begründung (für die UI).""" + ctx = max_ctx_for(params_b, quant, sys_ram_gb) + k = ctx // 1024 + return {"ctx": ctx, "k": k, + "note": f"Bis ~{k}k Kontext passt komfortabel auf deine Hardware ({round(sys_ram_gb)} GB)."} diff --git a/routers/cookbook.py b/routers/cookbook.py index f99e70f..8b24d53 100644 --- a/routers/cookbook.py +++ b/routers/cookbook.py @@ -9,7 +9,7 @@ from pydantic import BaseModel import psutil from auth import auth -from hw_math import evaluate_fit +from hw_math import evaluate_fit, max_ctx_for router = APIRouter(prefix="/api/cookbook", dependencies=[Depends(auth)]) @@ -79,6 +79,7 @@ async def analyze_repo(req: AnalyzeRequest): "filename": f, "quant": quant, "fit": fit, + "optimal_ctx": max_ctx_for(params_b, quant, ram_gb), "priority": priority }) @@ -96,5 +97,6 @@ async def analyze_repo(req: AnalyzeRequest): def evaluate_single(req: EvaluateRequest): ram_gb = psutil.virtual_memory().total / (1024**3) fit = evaluate_fit(req.params_b, req.quant, req.ctx, ram_gb) + fit["optimal_ctx"] = max_ctx_for(req.params_b, req.quant, ram_gb) return fit diff --git a/routers/models.py b/routers/models.py index ee0bafb..24a1e69 100644 --- a/routers/models.py +++ b/routers/models.py @@ -16,8 +16,10 @@ from auth import auth from config import CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, LLAMA_SWAP_URL, MODELS_DIR, TOKEN from jobengine import JOBS, start_job from llamaswap import _swap_get, read_config, write_config +from hw_math import extract_params_b, max_ctx_for import re import os +import psutil router = APIRouter(prefix="/api", dependencies=[Depends(auth)]) @@ -54,6 +56,10 @@ class UpdateReq(BaseModel): @router.get("/status") def status(): cfg = read_config() + try: + ram_gb = psutil.virtual_memory().total / (1024 ** 3) + except Exception: # noqa: BLE001 + ram_gb = 0 configured = {} for name, spec in (cfg.get("models") or {}).items(): spec = spec or {} @@ -94,7 +100,9 @@ def status(): "size_bytes": size_bytes, "quant": quant, "caps": caps, - "filename": filename + "filename": filename, + "params_b": extract_params_b(filename or name), + "optimal_ctx": (max_ctx_for(extract_params_b(filename or name), quant or "Q4_K_M", ram_gb) if ram_gb else None), } } swap_ok = True diff --git a/static/js/panels/cookbook.js b/static/js/panels/cookbook.js index 87327fe..d75ba94 100644 --- a/static/js/panels/cookbook.js +++ b/static/js/panels/cookbook.js @@ -74,6 +74,7 @@ function mount() {
+
Ressourcen-Check
@@ -94,6 +95,7 @@ function mount() { $("#cb-m-download").addEventListener("click", doDownload); $("#cb-m-files").addEventListener("change", updateLiveFit); $("#cb-m-ctx").addEventListener("change", reanalyzeCtx); + window.cbSetCtx = v => { $("#cb-m-ctx").value = v; reanalyzeCtx(); }; renderHwChip(); renderCurated(); @@ -207,6 +209,10 @@ function showFit() { $("#cb-m-fit").style.display = "flex"; $("#cb-m-fit-text").innerHTML = `Bedarf: ~${f.fit.req_gb.toFixed(1)} GB · ${currentAnalysis.params_b}B · ${esc(f.quant)} · ~${Math.round(f.fit.tps)} Tok/s`; $("#cb-m-fit-badge").innerHTML = `${esc(f.fit.text)}`; + const opt = f.optimal_ctx, recEl = $("#cb-m-ctx-rec"); + if (recEl) recEl.innerHTML = opt + ? `Empfohlener Kontext für deine Hardware: ~${Math.round(opt / 1024)}kübernehmen` + : ""; const btn = $("#cb-m-download"); if (f.fit.level === "too_tight") { btn.className = "primary warn"; btn.textContent = "Trotzdem holen (zu groß)"; } else { btn.className = "primary"; btn.textContent = "Herunterladen & Einpflegen"; } @@ -255,7 +261,7 @@ async function openCurated(i) { $("#cb-m-alias").value = m.alias; $("#cb-m-ctx").value = m.ctx; $("#cb-m-download").disabled = false; try { const fit = await api("/api/cookbook/evaluate", { method: "POST", body: JSON.stringify({ params_b: m.params_b, quant: m.quant, ctx: m.ctx }) }); - currentAnalysis = { repo: m.repo, params_b: m.params_b, files: [{ filename: m.file, quant: m.quant, fit }] }; + currentAnalysis = { repo: m.repo, params_b: m.params_b, files: [{ filename: m.file, quant: m.quant, fit, optimal_ctx: fit.optimal_ctx }] }; showFit(); } catch {} } diff --git a/static/js/panels/models.js b/static/js/panels/models.js index a94c840..747a0fc 100644 --- a/static/js/panels/models.js +++ b/static/js/panels/models.js @@ -56,6 +56,11 @@ function mount() {
Höhere Werte erlauben längere Texte, brauchen aber mehr Grafikspeicher.
+
`; @@ -97,6 +102,14 @@ function openConfig(alias) { const m = ALL.find(x => x.name === alias); if (!m) return; $("#cfg-model-name").textContent = m.name; $("#cfg-ctx").value = m.meta?.ctx || 8192; + const opt = m.meta?.optimal_ctx, rec = $("#cfg-rec"); + if (opt) { + const cur = m.meta?.ctx || 0; + $("#cfg-rec-note").textContent = + `Bis ~${Math.round(opt / 1024)}k passt komfortabel. Aktuell: ~${Math.round(cur / 1024)}k.`; + $("#cfg-rec-apply").onclick = () => { $("#cfg-ctx").value = opt; }; + rec.style.display = "flex"; + } else { rec.style.display = "none"; } $("#cfg-modal").style.display = "flex"; } async function saveConfig() {