diff --git a/deploy/pruefstand/pruefstand.sh b/deploy/pruefstand/pruefstand.sh index d6bfb35..8e365bc 100644 --- a/deploy/pruefstand/pruefstand.sh +++ b/deploy/pruefstand/pruefstand.sh @@ -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 diff --git a/deploy/trend-radar-feed.sh b/deploy/trend-radar-feed.sh index 8c9ef01..959a38f 100644 --- a/deploy/trend-radar-feed.sh +++ b/deploy/trend-radar-feed.sh @@ -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