Pruefstand+Radar: MTP-Fairness, Tempo-Baseline-Refresh, Puls-Langzeit-Historie

Drei Luecken aus der Selbst-Review 17.07.:
1. MTP-Fairness: Kandidaten mit MTP-Kopf (Dateiname oder nextn-Tensoren im
   GGUF-Kopf) bekommen eine Zusatz-Tempomessung mit draft-mtp; Steckbrief
   rechnet best-of speed/speed_mtp - das Live-Hirn laeuft schliesslich MIT MTP.
2. Baseline-Refresh: baseline.json traegt jetzt den Engine-Build; weicht die
   installierte Engine ab (So-Auto-Update), misst der Pruefstand-Cron nur den
   Tempo-Teil aller Rollen neu (Geister-Zahlen-Schutz, Qualitaet bleibt gueltig).
3. Langzeit-Historie: Puls wird als JSONL angehaengt statt ueberschrieben;
   Radar vergleicht zusaetzlich gegen ~8 Wochen zurueck und meldet
   SCHLEICHENDE REGRESSION, die woechentlich unter der 10-%-Schwelle bleibt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-17 16:28:27 +02:00
parent 9c03316755
commit de21e1ac68
2 changed files with 155 additions and 24 deletions
+105 -17
View File
@@ -55,6 +55,61 @@ PY
)" >/dev/null 2>&1 || true
}
engine_build() { # installierte llama.cpp-Build-Nummer (leer, wenn nicht ermittelbar)
LD_LIBRARY_PATH="$(dirname "$BENCH_BIN")" "$BENCH_BIN" --version 2>&1 \
| grep -o 'version: [0-9]*' | grep -o '[0-9]*' || true
}
aggregiere_baseline() { # $1=Engine-Build — baut baseline.json aus den Rollen-Dateien
python3 - "$STATE" "${1:-}" <<'PY'
import glob, json, os, sys, time
state, build = sys.argv[1], sys.argv[2]
basis = {}
for f in glob.glob(os.path.join(state, "ergebnisse", "baseline-*.json")):
rolle = os.path.basename(f)[len("baseline-"):-len(".json")]
try:
basis[rolle] = json.load(open(f)).get("zusammenfassung", {})
except Exception:
pass
json.dump({"erhoben": time.strftime("%Y-%m-%d %H:%M"), "engine": build,
"rollen": basis},
open(os.path.join(state, "baseline.json"), "w"), ensure_ascii=False, indent=1)
print(f"\nBasislinie gespeichert ({len(basis)} Rollen, Engine b{build or '?'}).")
PY
}
# Tempo-Teil der Baseline neu messen (Lücke 2, 17.07.): Engine-Updates (So 04:30)
# verschieben das Tempo der Amtsinhaber — die Qualitäts-Zahlen bleiben gültig, aber
# Tempo-Vergleiche gegen eine alte Engine wären Geister-Zahlen.
speed_refresh() { # $1=neue Engine-Build-Nummer
local BUILD="$1"
echo "## PRÜFSTAND: Tempo-Baseline-Refresh (Engine jetzt b${BUILD}) am $(date '+%d.%m.%Y %H:%M')"
local rolle TMP
for rolle in hermes coder heavy vision scout; do
TMP="$STATE/ergebnisse/refresh-${rolle}.json"
echo "### $rolle"
python3 "$PSD/harness.py" --endpoint "$LIVE" --model "$rolle" --suites speed \
--out "$TMP" --judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \
--deadline-min 10 2>&1 | tail -2
python3 - "$STATE/ergebnisse/baseline-${rolle}.json" "$TMP" <<'PY'
import json, sys
try:
basis = json.load(open(sys.argv[1]))
neu = json.load(open(sys.argv[2]))
except Exception:
raise SystemExit
if "speed" in (neu.get("zusammenfassung") or {}):
basis.setdefault("zusammenfassung", {})["speed"] = neu["zusammenfassung"]["speed"]
basis["einzeln"] = ([e for e in basis.get("einzeln", []) if e.get("suite") != "speed"]
+ [e for e in neu.get("einzeln", []) if e.get("suite") == "speed"])
json.dump(basis, open(sys.argv[1], "w"), ensure_ascii=False, indent=1)
PY
rm -f "$TMP"
done
aggregiere_baseline "$BUILD"
briefkasten "Prüfstand" "Tempo-Baseline nach Engine-Wechsel neu gemessen (jetzt b${BUILD})."
}
zusammenfassung_lesen() { # $1=ergebnis.json → kompakte Zeilen "suite: x/y · Note · tg"
python3 - "$1" <<'PY'
import json, sys
@@ -103,20 +158,7 @@ except Exception as e:
print(f" FEHLER: {e}")
PY
# Sammel-Baseline schreiben (eine Datei, die Kandidaten-Läufe vergleichen)
python3 - "$STATE" <<'PY'
import glob, json, os, sys, time
state = sys.argv[1]
basis = {}
for f in glob.glob(os.path.join(state, "ergebnisse", "baseline-*.json")):
rolle = os.path.basename(f)[len("baseline-"):-len(".json")]
try:
basis[rolle] = json.load(open(f)).get("zusammenfassung", {})
except Exception:
pass
json.dump({"erhoben": time.strftime("%Y-%m-%d %H:%M"), "rollen": basis},
open(os.path.join(state, "baseline.json"), "w"), ensure_ascii=False, indent=1)
print(f"\nBasislinie gespeichert ({len(basis)} Rollen).")
PY
aggregiere_baseline "$(engine_build)"
briefkasten "Prüfstand" "Basislinie der Amtsinhaber erhoben — künftige Kandidaten werden dagegen verglichen."
}
@@ -259,6 +301,45 @@ PY
--deadline-min "$DEADLINE_MIN" 2>&1 | tail -8
kill $SPID 2>/dev/null; wait $SPID 2>/dev/null; trap - RETURN
# ---- MTP-Fairness (Lücke 1, 17.07.): das Live-Hirn läuft MIT draft-mtp (+26 %) —
# ein Kandidat mit eingebautem MTP-Kopf würde ohne Zusatzmessung systematisch
# unterschätzt. Erkennung: Dateiname ODER nextn-Tensoren im GGUF-Kopf.
if printf '%s' "$PFAD" | grep -qi 'mtp' || head -c 8000000 "$ZIEL" 2>/dev/null | grep -aq 'nextn'; then
echo "MTP-Kopf erkannt — Zusatzmessung mit draft-mtp (Fairness gegen das Live-Hirn) …"
$BENCH_BIN -m "$ZIEL" --host 127.0.0.1 --port "$BENCH_PORT" \
-c 32768 -ngl 999 -fa on --no-mmap --jinja $MMFLAG \
--spec-type draft-mtp --spec-draft-n-max 3 \
>/tmp/pruefstand-server-mtp.log 2>&1 &
local MPID=$!
trap 'kill $MPID 2>/dev/null; wait $MPID 2>/dev/null' RETURN
local MBEREIT=0
for i in $(seq 1 120); do
curl -s --max-time 2 "http://127.0.0.1:$BENCH_PORT/health" | grep -q ok && { MBEREIT=1; break; }
sleep 2
kill -0 $MPID 2>/dev/null || break
done
if [ "$MBEREIT" = "1" ]; then
local MOUT="$STATE/ergebnisse/kandidat-${KEY}-mtp.json"
python3 "$PSD/harness.py" --endpoint "http://127.0.0.1:$BENCH_PORT/v1" \
--model "kandidat" --suites speed --out "$MOUT" \
--judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" --deadline-min 10 2>&1 | tail -2
python3 - "$OUT" "$MOUT" <<'PY'
import json, sys
haupt = json.load(open(sys.argv[1]))
try:
s = (json.load(open(sys.argv[2])).get("zusammenfassung") or {}).get("speed")
except Exception:
s = None
if s:
haupt.setdefault("zusammenfassung", {})["speed_mtp"] = s
json.dump(haupt, open(sys.argv[1], "w"), ensure_ascii=False, indent=1)
PY
else
echo "Kandidat startet mit draft-mtp NICHT — Zusatzmessung entfällt (im Steckbrief zählt der Basis-Wert)."
fi
kill $MPID 2>/dev/null; wait $MPID 2>/dev/null; trap - RETURN
fi
# ---- Steckbrief: Kandidat vs. Amtsinhaber
local BERICHT="$STATE/ergebnisse/kandidat-${KEY}.md"
python3 - "$OUT" "$STATE/baseline.json" "$ROLLE" "$HF_ID" "$WARUM" "$BERICHT" <<'PY'
@@ -284,11 +365,12 @@ for name in sorted(set(kz) | set(basis)):
if s.get("pp_mittel"): t += f" · pp {s['pp_mittel']}"
return t
zeilen.append(f"| {name} | {fmt(kz.get(name))} | {fmt(basis.get(name))} |")
if name in kz and name in basis and name != "speed":
if name in kz and name in basis and not name.startswith("speed"):
faelle += 1
punkte_k += kz[name]["bestanden"] / max(1, kz[name]["gesamt"])
punkte_b += basis[name]["bestanden"] / max(1, basis[name]["gesamt"])
tg_k = (kz.get("speed") or {}).get("tg_mittel") or 0
# Tempo: best-of speed/speed_mtp — die Baseline ist live MIT MTP gemessen (Fairness)
tg_k = max(((kz.get(s) or {}).get("tg_mittel") or 0) for s in ("speed", "speed_mtp"))
tg_b = (basis.get("speed") or {}).get("tg_mittel") or 0
zeilen.append("")
if faelle:
@@ -339,10 +421,16 @@ case "${1:-}" in
baseline_lauf
exit 0
fi
# Engine gewechselt seit der Baseline? → Tempo-Teil neu messen (Geister-Zahlen-Schutz)
BUILD="$(engine_build)"
BASE_BUILD="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("engine") or "")' "$STATE/baseline.json" 2>/dev/null || true)"
if [ -n "$BUILD" ] && [ "$BUILD" != "$BASE_BUILD" ]; then
speed_refresh "$BUILD"
fi
NAECHSTER="$(ls -1tr "$STATE/queue/"*.json 2>/dev/null | head -1 || true)"
if [ -n "$NAECHSTER" ]; then
kandidat_lauf "$NAECHSTER"
fi
# keine Queue → LEERER stdout → --no-agent-Cron bleibt still (bewusst)
# keine Queue + keine Engine-Drift → LEERER stdout → --no-agent-Cron bleibt still
;;
esac
+50 -7
View File
@@ -21,6 +21,7 @@ set -uo pipefail
MC="$HOME/mission-control-v2"
STATE_DIR="$HOME/.hermes/state"
STATE="$STATE_DIR/trend-radar-state.json"
HIST="$STATE_DIR/trend-radar-puls-history.jsonl" # Langzeit-Verlauf (Lücke 3, 17.07.)
PS_QUEUE="$STATE_DIR/pruefstand/queue"
PS_DONE="$STATE_DIR/pruefstand/done"
WATCHLIST="$MC/deploy/trend-radar-watchlist.json"
@@ -61,11 +62,12 @@ echo
# =====================================================================
# 1. LIVE-PULS aller Rollen (heavy zuletzt → Analyst findet es noch warm)
# =====================================================================
echo "### 1. LIVE-PULS (echte Messung über llama-swap, Vergleich zur Vorwoche)"
python3 - "$ENDPOINT" "$STATE" "$TMPD/puls.json" <<'PY'
echo "### 1. LIVE-PULS (echte Messung über llama-swap, Vergleich zur Vorwoche + Langzeit)"
python3 - "$ENDPOINT" "$STATE" "$TMPD/puls.json" "$HIST" <<'PY'
import json, sys, time, urllib.request
endpoint, state_pfad, out_pfad = sys.argv[1], sys.argv[2], sys.argv[3]
hist_pfad = sys.argv[4]
ROLLEN = ["hermes", "coder", "vision", "scout", "heavy"] # heavy zuletzt (Analyst-Wärme)
def chat(model, timeout=420):
@@ -117,6 +119,40 @@ for rolle, w in puls.items():
if d <= -10:
zeile += " ⚠️ REGRESSION"
print(zeile)
# Langzeit-Vergleich (~8 Wochen): fängt SCHLEICHENDE Regression, die jede Woche
# einzeln unter der 10-%-Schwelle bleibt (Lücke 3, 17.07.).
eintraege = []
try:
with open(hist_pfad, encoding="utf-8") as f:
for l in f:
l = l.strip()
if l:
try:
eintraege.append(json.loads(l))
except Exception:
pass
except FileNotFoundError:
pass
grenze = time.time() - 49 * 86400
alte = [z for z in eintraege if (z.get("ts") or 0) <= grenze]
ref = alte[-1] if alte else None
if ref:
treffer = []
for rolle, w in puls.items():
a = (ref.get("puls") or {}).get(rolle) or {}
if a.get("tg") and w.get("tg"):
d = 100 * w["tg"] / a["tg"] - 100
if d <= -10:
treffer.append(f"- LANGZEIT {rolle}: tg {w['tg']} vs. {a['tg']} "
f"({ref.get('datum','?')}, Engine b{ref.get('engine','?')}) "
f"= {d:+.0f} % ⚠️ SCHLEICHENDE REGRESSION")
if treffer:
print(f"Langzeit-Vergleich (vs. {ref.get('datum','?')}):")
for t in treffer:
print(t)
else:
print(f"Langzeit-Vergleich vs. {ref.get('datum','?')}: unauffällig.")
json.dump(puls, open(out_pfad, "w"))
PY
PULS_BLOCK="$(cat "$TMPD/puls.json" 2>/dev/null || echo '{}')"
@@ -408,17 +444,24 @@ PY
echo "### 6. PRÜFSTAND-QUEUE"
echo "$ERGEBNIS"
# ---------- Puls in den State (für den Vorwochen-Vergleich) ----------
python3 - "$STATE" "$TMPD/puls.json" <<'PY'
import json, sys
# ---------- Puls in den State (Vorwoche) + an die Langzeit-Historie anhängen ----------
python3 - "$STATE" "$TMPD/puls.json" "$HIST" "${INST_NUM:-}" <<'PY'
import json, sys, time
try:
state = json.load(open(sys.argv[1]))
except Exception:
state = {}
try:
state["puls"] = json.load(open(sys.argv[2]))
puls = json.load(open(sys.argv[2]))
except Exception:
pass
puls = None
if puls:
state["puls"] = puls
with open(sys.argv[3], "a", encoding="utf-8") as f:
f.write(json.dumps({"ts": int(time.time()),
"datum": time.strftime("%Y-%m-%d"),
"engine": sys.argv[4], "puls": puls},
ensure_ascii=False) + "\n")
json.dump(state, open(sys.argv[1], "w"))
PY