Feat: fit-aware Brain-Update (Speicher-Budget) + brains-Kontext gesenkt (Always-On-Footprint)
- Brain-Kontexte gesenkt (live config): hermes 128K->64K, fast/vision 128K->32K -> Always-Warm-Footprint 74GB->51GB, ~23GB frei; heavy/coder passen wieder daneben. - /api/agent/brain liefert jetzt `budget`: projiziert den Always-On-Footprint mit dem empfohlenen Brain gegen das GTT-Budget (aus amdgpu.gttsize) und prueft, ob das groesste on-demand-Modell daneben passt (fit.estimate_memory_gb). - Cockpit Agent-Hirn-Karte: Budget-Zeile (gruen/rot) + Warnung im Update-Confirm, wenn ein zu grosses Always-On-Brain das groesste on-demand-Modell verdraengen wuerde. Button wird rot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,19 @@ def _hermes_version(name: str) -> float | None:
|
|||||||
return float(m.group(1)) if m else None
|
return float(m.group(1)) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def _gtt_budget_gb() -> float:
|
||||||
|
"""GPU-adressierbarer Speicher (GTT) in GB — die harte Obergrenze. Liest
|
||||||
|
amdgpu.gttsize aus /proc/cmdline, sonst RAM minus OS-Reserve."""
|
||||||
|
try:
|
||||||
|
with open("/proc/cmdline") as f:
|
||||||
|
m = re.search(r"amdgpu\.gttsize=(\d+)", f.read())
|
||||||
|
if m:
|
||||||
|
return round(int(m.group(1)) / 1024.0, 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return round(psutil.virtual_memory().total / (1024 ** 3) - 6.0, 1)
|
||||||
|
|
||||||
|
|
||||||
def hermes_brain_info() -> dict:
|
def hermes_brain_info() -> dict:
|
||||||
"""Aktuelles Agent-Hirn (hermes-Rolle) + bestes verfügbares NousResearch-Hermes-Modell,
|
"""Aktuelles Agent-Hirn (hermes-Rolle) + bestes verfügbares NousResearch-Hermes-Modell,
|
||||||
das auf diese Hardware passt. Für den Modell-Manager: Brain sichtbar + updatebar,
|
das auf diese Hardware passt. Für den Modell-Manager: Brain sichtbar + updatebar,
|
||||||
@@ -75,7 +88,45 @@ def hermes_brain_info() -> dict:
|
|||||||
# gleiche Datei schon installiert? dann kein Update
|
# gleiche Datei schon installiert? dann kein Update
|
||||||
if current and best["repo"].split("/")[-1].lower() in (current["name"] or "").lower():
|
if current and best["repo"].split("/")[-1].lower() in (current["name"] or "").lower():
|
||||||
update = False
|
update = False
|
||||||
return {"current": current, "recommended": best, "update_available": update}
|
|
||||||
|
# Fit-Check: passt das EMPFOHLENE Brain als Always-On noch ins Budget, sodass das
|
||||||
|
# größte on-demand-Modell daneben lädt? (Brain muss immer resident sein.)
|
||||||
|
budget = None
|
||||||
|
try:
|
||||||
|
from services.fit import estimate_memory_gb
|
||||||
|
groups = llamaswap.list_groups()
|
||||||
|
persist = set()
|
||||||
|
for g in groups.values():
|
||||||
|
if isinstance(g, dict) and g.get("persist"):
|
||||||
|
persist.update(g.get("members") or [])
|
||||||
|
|
||||||
|
def _foot(m: dict) -> float:
|
||||||
|
caps = m.get("capabilities") or {}
|
||||||
|
return estimate_memory_gb(float(caps.get("params_b") or 7.0),
|
||||||
|
m.get("quant") or "Q4_K_M", int(m.get("ctx") or 32768))
|
||||||
|
|
||||||
|
cur_name = cur["name"] if cur else None
|
||||||
|
brain_ctx = int((cur.get("ctx") if cur else None) or 32768)
|
||||||
|
# Always-Warm = persist-Modelle, das Brain durch die Empfehlung ersetzt
|
||||||
|
warm = sum(_foot(m) for m in models if m["name"] in persist and m["name"] != cur_name)
|
||||||
|
if best:
|
||||||
|
warm += estimate_memory_gb(float(best["params_b"]), "Q4_K_M", brain_ctx)
|
||||||
|
elif cur:
|
||||||
|
warm += _foot(cur)
|
||||||
|
largest_od = max((_foot(m) for m in models if m["name"] not in persist), default=0.0)
|
||||||
|
gtt = _gtt_budget_gb()
|
||||||
|
free_after = gtt - warm
|
||||||
|
budget = {
|
||||||
|
"gtt_gb": gtt,
|
||||||
|
"warm_projected_gb": round(warm, 1),
|
||||||
|
"free_after_gb": round(free_after, 1),
|
||||||
|
"largest_ondemand_gb": round(largest_od, 1),
|
||||||
|
"fits": free_after >= largest_od,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
log.debug("hermes_brain_info: Budget-Berechnung fehlgeschlagen", exc_info=True)
|
||||||
|
|
||||||
|
return {"current": current, "recommended": best, "update_available": update, "budget": budget}
|
||||||
|
|
||||||
|
|
||||||
def _reach(url: str, path: str = "") -> bool:
|
def _reach(url: str, path: str = "") -> bool:
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+52
-50
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BPfAqjxb.js"></script>
|
<script type="module" crossorigin src="/assets/index-eiqRvBvu.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Bgft9fxe.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D4NZ6il2.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -315,8 +315,17 @@ export interface HermesBrainCandidate {
|
|||||||
fit: Fit
|
fit: Fit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HermesBrainBudget {
|
||||||
|
gtt_gb: number
|
||||||
|
warm_projected_gb: number // Always-On-Footprint mit dem empfohlenen Brain
|
||||||
|
free_after_gb: number
|
||||||
|
largest_ondemand_gb: number // größtes on-demand-Modell (z.B. heavy)
|
||||||
|
fits: boolean // passt das größte on-demand daneben?
|
||||||
|
}
|
||||||
|
|
||||||
export interface HermesBrainResp {
|
export interface HermesBrainResp {
|
||||||
current: HermesBrainModel | null
|
current: HermesBrainModel | null
|
||||||
recommended: HermesBrainCandidate | null
|
recommended: HermesBrainCandidate | null
|
||||||
update_available: boolean
|
update_available: boolean
|
||||||
|
budget?: HermesBrainBudget | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,9 +177,13 @@ export function Cockpit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleBrainUpdate(repo: string) {
|
async function handleBrainUpdate(repo: string) {
|
||||||
|
const b = brain?.budget
|
||||||
|
const warn = b && !b.fits
|
||||||
|
? `\n\n⚠ Speicher-Warnung: Als Always-On würde dieses Brain ~${b.warm_projected_gb} GB belegen (Budget ${b.gtt_gb} GB) → nur ${b.free_after_gb} GB frei. Das größte on-demand-Modell (~${b.largest_ondemand_gb} GB) passt dann NICHT mehr gleichzeitig daneben. Erwäge ein kleineres Brain oder weniger Kontext.`
|
||||||
|
: ""
|
||||||
showConfirm(
|
showConfirm(
|
||||||
"Agent-Hirn aktualisieren?",
|
"Agent-Hirn aktualisieren?",
|
||||||
`Neues Hermes-Modell '${repo.split("/").pop()}' herunterladen und als Agent-Hirn (Rolle hermes) setzen? Hermes nutzt danach automatisch das neue Modell.`,
|
`Neues Hermes-Modell '${repo.split("/").pop()}' herunterladen und als Agent-Hirn (Rolle hermes) setzen? Hermes nutzt danach automatisch das neue Modell.${warn}`,
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await api("/api/models/install", {
|
await api("/api/models/install", {
|
||||||
@@ -676,7 +680,12 @@ export function Cockpit() {
|
|||||||
{brain.update_available && brain.recommended ? (
|
{brain.update_available && brain.recommended ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleBrainUpdate(brain.recommended!.repo)}
|
onClick={() => handleBrainUpdate(brain.recommended!.repo)}
|
||||||
className="h-8 px-3 shrink-0 flex items-center justify-center gap-1.5 rounded-lg bg-amber-500 text-black text-[10px] font-bold uppercase hover:bg-amber-400 transition-all cursor-pointer shadow-md shadow-amber-500/10"
|
className={cn(
|
||||||
|
"h-8 px-3 shrink-0 flex items-center justify-center gap-1.5 rounded-lg text-[10px] font-bold uppercase transition-all cursor-pointer shadow-md",
|
||||||
|
brain.budget && !brain.budget.fits
|
||||||
|
? "bg-red-500 text-white hover:bg-red-400 shadow-red-500/10"
|
||||||
|
: "bg-amber-500 text-black hover:bg-amber-400 shadow-amber-500/10"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Download className="h-3.5 w-3.5" /> Aktualisieren
|
<Download className="h-3.5 w-3.5" /> Aktualisieren
|
||||||
</button>
|
</button>
|
||||||
@@ -694,6 +703,20 @@ export function Cockpit() {
|
|||||||
(v{brain.recommended.version}, {brain.recommended.params_b}B) — von NousResearch.
|
(v{brain.recommended.version}, {brain.recommended.params_b}B) — von NousResearch.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{brain.budget && (
|
||||||
|
<div className={cn("text-[10px] flex items-start gap-1.5 leading-relaxed",
|
||||||
|
brain.budget.fits ? "text-muted-foreground/80" : "text-red-400 font-semibold")}>
|
||||||
|
<HardDrive className="h-3 w-3 shrink-0 mt-0.5" />
|
||||||
|
<span>
|
||||||
|
{brain.update_available ? "Mit diesem Brain als Always-On: " : "Always-On-Budget: "}
|
||||||
|
~{brain.budget.warm_projected_gb} / {brain.budget.gtt_gb} GB belegt → {brain.budget.free_after_gb} GB frei
|
||||||
|
{brain.budget.fits
|
||||||
|
? ` · größtes on-demand-Modell (~${brain.budget.largest_ondemand_gb} GB) passt daneben ✓`
|
||||||
|
: ` · größtes on-demand-Modell (~${brain.budget.largest_ondemand_gb} GB) passt NICHT mehr daneben ⚠ — kleineres Brain/weniger Kontext`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user