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 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-21 13:13:30 +02:00
parent f3da24a712
commit 654720ed05
5 changed files with 75 additions and 3 deletions
+43
View File
@@ -3,6 +3,8 @@ Extrahierte Mathematik aus dem Odysseus Projekt zur VRAM/RAM Berechnung.
Abgestimmt auf APUs mit Unified Memory (Bosgame M5 / Strix Halo). Abgestimmt auf APUs mit Unified Memory (Bosgame M5 / Strix Halo).
""" """
import re
# Annahme: Bytes per Parameter für GGUF Quants # Annahme: Bytes per Parameter für GGUF Quants
QUANT_BYTES_PER_PARAM = { QUANT_BYTES_PER_PARAM = {
"Q2_K": 0.35, "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), "req_gb": round(req_gb, 1),
"tps": round(tps, 0) "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)."}
+3 -1
View File
@@ -9,7 +9,7 @@ from pydantic import BaseModel
import psutil import psutil
from auth import auth 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)]) router = APIRouter(prefix="/api/cookbook", dependencies=[Depends(auth)])
@@ -79,6 +79,7 @@ async def analyze_repo(req: AnalyzeRequest):
"filename": f, "filename": f,
"quant": quant, "quant": quant,
"fit": fit, "fit": fit,
"optimal_ctx": max_ctx_for(params_b, quant, ram_gb),
"priority": priority "priority": priority
}) })
@@ -96,5 +97,6 @@ async def analyze_repo(req: AnalyzeRequest):
def evaluate_single(req: EvaluateRequest): def evaluate_single(req: EvaluateRequest):
ram_gb = psutil.virtual_memory().total / (1024**3) ram_gb = psutil.virtual_memory().total / (1024**3)
fit = evaluate_fit(req.params_b, req.quant, req.ctx, ram_gb) 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 return fit
+9 -1
View File
@@ -16,8 +16,10 @@ from auth import auth
from config import CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, LLAMA_SWAP_URL, MODELS_DIR, TOKEN from config import CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, LLAMA_SWAP_URL, MODELS_DIR, TOKEN
from jobengine import JOBS, start_job from jobengine import JOBS, start_job
from llamaswap import _swap_get, read_config, write_config from llamaswap import _swap_get, read_config, write_config
from hw_math import extract_params_b, max_ctx_for
import re import re
import os import os
import psutil
router = APIRouter(prefix="/api", dependencies=[Depends(auth)]) router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
@@ -54,6 +56,10 @@ class UpdateReq(BaseModel):
@router.get("/status") @router.get("/status")
def status(): def status():
cfg = read_config() cfg = read_config()
try:
ram_gb = psutil.virtual_memory().total / (1024 ** 3)
except Exception: # noqa: BLE001
ram_gb = 0
configured = {} configured = {}
for name, spec in (cfg.get("models") or {}).items(): for name, spec in (cfg.get("models") or {}).items():
spec = spec or {} spec = spec or {}
@@ -94,7 +100,9 @@ def status():
"size_bytes": size_bytes, "size_bytes": size_bytes,
"quant": quant, "quant": quant,
"caps": caps, "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 swap_ok = True
+7 -1
View File
@@ -74,6 +74,7 @@ function mount() {
<div><label>Alias (Rolle)</label><input id="cb-m-alias" placeholder="z.B. coder"></div> <div><label>Alias (Rolle)</label><input id="cb-m-alias" placeholder="z.B. coder"></div>
<div><label>Kontext-Größe</label><input id="cb-m-ctx" type="number" value="8192"></div> <div><label>Kontext-Größe</label><input id="cb-m-ctx" type="number" value="8192"></div>
</div> </div>
<div class="hint" id="cb-m-ctx-rec" style="margin:-6px 0 14px"></div>
<div id="cb-m-fit" class="tile" style="display:flex;justify-content:space-between;align-items:center;margin:8px 0 18px"> <div id="cb-m-fit" class="tile" style="display:flex;justify-content:space-between;align-items:center;margin:8px 0 18px">
<div><div style="font-size:13px">Ressourcen-Check</div> <div><div style="font-size:13px">Ressourcen-Check</div>
@@ -94,6 +95,7 @@ function mount() {
$("#cb-m-download").addEventListener("click", doDownload); $("#cb-m-download").addEventListener("click", doDownload);
$("#cb-m-files").addEventListener("change", updateLiveFit); $("#cb-m-files").addEventListener("change", updateLiveFit);
$("#cb-m-ctx").addEventListener("change", reanalyzeCtx); $("#cb-m-ctx").addEventListener("change", reanalyzeCtx);
window.cbSetCtx = v => { $("#cb-m-ctx").value = v; reanalyzeCtx(); };
renderHwChip(); renderHwChip();
renderCurated(); renderCurated();
@@ -207,6 +209,10 @@ function showFit() {
$("#cb-m-fit").style.display = "flex"; $("#cb-m-fit").style.display = "flex";
$("#cb-m-fit-text").innerHTML = `Bedarf: <b>~${f.fit.req_gb.toFixed(1)} GB</b> · ${currentAnalysis.params_b}B · ${esc(f.quant)} · ~${Math.round(f.fit.tps)} Tok/s`; $("#cb-m-fit-text").innerHTML = `Bedarf: <b>~${f.fit.req_gb.toFixed(1)} GB</b> · ${currentAnalysis.params_b}B · ${esc(f.quant)} · ~${Math.round(f.fit.tps)} Tok/s`;
$("#cb-m-fit-badge").innerHTML = `<span class="fit-badge ${fitCls(f.fit.level)}">${esc(f.fit.text)}</span>`; $("#cb-m-fit-badge").innerHTML = `<span class="fit-badge ${fitCls(f.fit.level)}">${esc(f.fit.text)}</span>`;
const opt = f.optimal_ctx, recEl = $("#cb-m-ctx-rec");
if (recEl) recEl.innerHTML = opt
? `Empfohlener Kontext für deine Hardware: <b>~${Math.round(opt / 1024)}k</b> — <a href="#" onclick="event.preventDefault();window.cbSetCtx(${opt})">übernehmen</a>`
: "";
const btn = $("#cb-m-download"); const btn = $("#cb-m-download");
if (f.fit.level === "too_tight") { btn.className = "primary warn"; btn.textContent = "Trotzdem holen (zu groß)"; } if (f.fit.level === "too_tight") { btn.className = "primary warn"; btn.textContent = "Trotzdem holen (zu groß)"; }
else { btn.className = "primary"; btn.textContent = "Herunterladen & Einpflegen"; } 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; $("#cb-m-alias").value = m.alias; $("#cb-m-ctx").value = m.ctx; $("#cb-m-download").disabled = false;
try { try {
const fit = await api("/api/cookbook/evaluate", { method: "POST", body: JSON.stringify({ params_b: m.params_b, quant: m.quant, ctx: m.ctx }) }); 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(); showFit();
} catch {} } catch {}
} }
+13
View File
@@ -56,6 +56,11 @@ function mount() {
<label>Kontext-Größe (Tokens)</label> <label>Kontext-Größe (Tokens)</label>
<input id="cfg-ctx" type="number" value="8192"> <input id="cfg-ctx" type="number" value="8192">
<div class="hint">Höhere Werte erlauben längere Texte, brauchen aber mehr Grafikspeicher.</div> <div class="hint">Höhere Werte erlauben längere Texte, brauchen aber mehr Grafikspeicher.</div>
<div class="tile" id="cfg-rec" style="display:none;margin:0 0 14px;justify-content:space-between;align-items:center" >
<div style="flex:1"><div style="font-size:13px">Empfohlen für deine Hardware</div>
<div class="hint" id="cfg-rec-note" style="margin:4px 0 0"></div></div>
<button class="ghost" id="cfg-rec-apply">Optimal übernehmen</button>
</div>
<button class="primary" id="cfg-save" style="width:100%;margin-top:6px">Speichern</button> <button class="primary" id="cfg-save" style="width:100%;margin-top:6px">Speichern</button>
</div> </div>
</div>`; </div>`;
@@ -97,6 +102,14 @@ function openConfig(alias) {
const m = ALL.find(x => x.name === alias); if (!m) return; const m = ALL.find(x => x.name === alias); if (!m) return;
$("#cfg-model-name").textContent = m.name; $("#cfg-model-name").textContent = m.name;
$("#cfg-ctx").value = m.meta?.ctx || 8192; $("#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"; $("#cfg-modal").style.display = "flex";
} }
async function saveConfig() { async function saveConfig() {