Files
club-3090/scripts/lib/registry-emit.sh
T
noonghunnaandtekgnosis-net 6cafaf80b6 Operational robustness (#281): orphan-safe switch.sh · reboot-surviving vLLM · multi-GPU power sweep (#285)
Re-bases tekgnosis-net's #281/#282 onto master: (1) switch.sh registry-derived VARIANT_CONTAINER closed-world teardown (+--remove-orphans) — fixes beellama/ik-llama/sglang VRAM leak; (2) 29 vLLM composes restart: ${CLUB3090_RESTART:-unless-stopped} (reboot survival, opt-out knob); (3) power-cap-sweep.sh multi-GPU (board-power sum + cap restore). 3 new tests; suite 41/41. Closes #281, supersedes #282.

Co-Authored-By: tekgnosis-net <[email protected]>
2026-05-31 22:13:52 +05:00

392 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# Shared compose-registry shell emitter for switch.sh and launch.sh.
#
# Source this file, declare the destination arrays in the caller, then call
# derive_switch_variant_tables or derive_launch_variant_tables with ROOT_DIR.
registry_variant_rows() {
local root="$1"
python3 - "$root" <<'PY_EMIT'
from __future__ import annotations
import re
import sys
from pathlib import Path
import yaml
root = Path(sys.argv[1])
sys.path.insert(0, str(root))
from scripts.lib.profiles.compose_registry import COMPOSE_REGISTRY, DEFAULTS # noqa: E402
def die(key: str, message: str) -> None:
print(f"__ERR__\t{key}\t{message}")
def launch_engine(key: str) -> str:
prefix = key.split("/", 1)[0]
return "llamacpp" if prefix in {"llamacpp", "ik-llama"} else prefix
def switch_engine(key: str) -> str:
prefix = key.split("/", 1)[0]
return "llamacpp" if prefix == "llamacpp" else prefix
def container_name(compose_path: str) -> str:
path = root / compose_path
try:
data = yaml.safe_load(path.read_text()) or {}
except Exception as exc:
raise RuntimeError(f"could not parse compose yaml: {exc}") from exc
services = data.get("services") or {}
for service in services.values():
raw = service.get("container_name")
if not raw:
continue
raw = str(raw)
match = re.fullmatch(r"\$\{[^}:]+:-(.+)\}", raw)
return match.group(1) if match else raw
return ""
_CTX_ENV = re.compile(r"\$\{(?:MAX_MODEL_LEN|CTX_SIZE|CTX|MAX_CTX|N_CTX|MODEL_LEN)\s*:-\s*(\d+)\s*\}")
_CTX_FLAG = re.compile(r"(?:--max-model-len|--ctx-size|--n-ctx|(?<!\w)-c)\s*\n?\s*\"?(\d{3,})")
def compose_default_ctx(compose_path: str):
"""The ctx the compose serves by DEFAULT (its ${VAR:-N} fallback or flag literal)."""
try:
txt = (root / compose_path).read_text()
except Exception:
return None
m = _CTX_ENV.search(txt) or _CTX_FLAG.search(txt)
return int(m.group(1)) if m else None
def ctx_label(entry) -> str:
"""Compact ctx label rounded to K. Single 'NK' when the registry max_ctx matches
the compose's default ctx; 'NK/MK' (validated registry / compose default) when
they drift — so the list surfaces registry<->compose context mismatches."""
reg = entry.get("max_ctx")
if not reg:
return ""
reg = int(reg)
comp = compose_default_ctx(entry["compose_path"])
label = f"{round(reg / 1000)}K"
if comp is not None and comp != reg:
label += f"/{round(comp / 1000)}K"
return label
for key, entry in COMPOSE_REGISTRY.items():
cp = entry["compose_path"]
if "/compose/" not in cp:
die(key, f"compose_path lacks /compose/: {cp}")
continue
dirpart, filepart = cp.split("/compose/", 1)
compose_dir = f"{dirpart}/compose"
try:
cname = container_name(cp)
except Exception as exc:
die(key, str(exc))
continue
print(
"\t".join(
[
"VARIANT",
key,
switch_engine(key),
launch_engine(key),
compose_dir,
filepart,
str(entry["default_port"]),
str(entry["model"]),
str(entry["engine"]),
str(entry.get("kvcalc_key") or "SKIP"),
cname,
cp,
str(entry.get("status") or "production"),
# ctx label: 'NK' when registry max_ctx == compose default; 'NK/MK'
# (validated / compose) on drift. BEFORE status_note so the free-text
# note stays the LAST catch-all field.
ctx_label(entry),
# status_note is free text (may contain anything but a tab) — keep
# it as the LAST field so each reader's catch-all final var can
# absorb it without further splitting on its internal spaces.
(str(entry.get("status_note") or "").replace("\t", " ")),
]
)
)
for (model, engine, topology), target in DEFAULTS.items():
print("\t".join(["DEFAULT", model, engine, topology, target]))
PY_EMIT
}
derive_switch_variant_tables() {
local root="$1" emit key switch_engine _launch_engine cdir cfile port _model _profile_engine _kvcalc container _compose_path status max_ctx status_note
# Self-declare so every caller (switch.sh + test-switch-registry-parity) gets
# proper assoc arrays without each having to declare them. VARIANT_CONTAINER
# (slug -> container name) drives switch.sh's registry-derived orphan teardown.
declare -gA VARIANT_CTX VARIANT_CONTAINER
if ! emit="$(registry_variant_rows "$root" 2>/dev/null)"; then
echo "[switch] ERROR: could not derive variant tables from compose_registry.py" >&2
exit 2
fi
while IFS=$'\t' read -r kind key switch_engine _launch_engine cdir cfile port _model _profile_engine _kvcalc container _compose_path status max_ctx status_note; do
[[ -n "${kind:-}" ]] || continue
case "$kind" in
VARIANT)
if [[ "$key" == "__ERR__" ]]; then
echo "[switch] ERROR: registry entry not launchable: ${switch_engine} (${cdir})" >&2
exit 2
fi
VARIANTS["$key"]="${switch_engine}|${cdir}|${cfile}"
VARIANT_DEFAULT_PORT["$key"]="$port"
VARIANT_STATUS["$key"]="${status:-production}"
VARIANT_STATUS_NOTE["$key"]="${status_note:-}"
VARIANT_CTX["$key"]="${max_ctx:-}"
VARIANT_CONTAINER["$key"]="${container:-}"
;;
esac
done <<< "$emit"
if [[ ${#VARIANTS[@]} -eq 0 ]]; then
echo "[switch] ERROR: derived an empty variant table from compose_registry.py" >&2
exit 2
fi
}
derive_launch_variant_tables() {
local root="$1" emit key _switch_engine launch_engine cdir cfile port model profile_engine kvcalc container _compose_path status _max_ctx status_note
if ! emit="$(registry_variant_rows "$root" 2>/dev/null)"; then
echo "[launch] ERROR: could not derive variant tables from compose_registry.py" >&2
exit 2
fi
while IFS=$'\t' read -r kind key _switch_engine launch_engine cdir cfile port model profile_engine kvcalc container _compose_path status _max_ctx status_note; do
[[ -n "${kind:-}" ]] || continue
case "$kind" in
VARIANT)
if [[ "$key" == "__ERR__" ]]; then
echo "[launch] ERROR: registry entry not launchable: ${launch_engine} (${cdir})" >&2
exit 2
fi
LAUNCH_VARIANT_COMPOSE["$key"]="${cdir}/${cfile}"
LAUNCH_VARIANT_MODEL["$key"]="$model"
LAUNCH_VARIANT_ENGINE["$key"]="$launch_engine"
LAUNCH_VARIANT_PROFILE_ENGINE["$key"]="$profile_engine"
LAUNCH_VARIANT_KVCALC["$key"]="$kvcalc"
LAUNCH_DEFAULT_PORT["$key"]="$port"
LAUNCH_DEFAULT_CONTAINER["$key"]="$container"
LAUNCH_VARIANT_STATUS["$key"]="${status:-production}"
LAUNCH_VARIANT_STATUS_NOTE["$key"]="${status_note:-}"
LAUNCH_VARIANT_ORDER+=("$key")
;;
esac
done <<< "$emit"
if [[ ${#LAUNCH_VARIANT_COMPOSE[@]} -eq 0 ]]; then
echo "[launch] ERROR: derived an empty variant table from compose_registry.py" >&2
exit 2
fi
}
registry_default_target() {
local root="$1" model="$2" engine="$3" topology="$4"
python3 - "$root" "$model" "$engine" "$topology" <<'PY_DEFAULT'
from __future__ import annotations
import sys
from pathlib import Path
root = Path(sys.argv[1])
sys.path.insert(0, str(root))
from scripts.lib.profiles.compose_registry import DEFAULTS # noqa: E402
model, engine, topology = sys.argv[2:5]
target = DEFAULTS.get((model, engine, topology))
if target:
print(target)
raise SystemExit(0)
available = [
f"{m}/{e}/{t}->{v}"
for (m, e, t), v in sorted(DEFAULTS.items())
if m == model and e == engine
]
print(
"no default for "
f"model={model} engine={engine} topology={topology}. "
"Available defaults: " + (", ".join(available) if available else "<none>"),
file=sys.stderr,
)
raise SystemExit(1)
PY_DEFAULT
}
# --- PR-B: model-default resolver (the single injection point) ---------------
#
# model_default_target ROOT MODEL TOPOLOGY → resolved slug on stdout.
#
# Resolution precedence ladder (design §3); `--variant X` (caller-explicit) is
# handled by the callers BEFORE they reach here, so this implements:
# user pin (.env CLUB3090_DEFAULT_<MODEL>)
# ↓ else community seam (community_default_target → None today)
# ↓ else curated: ENGINE_PREFERENCE[topology] → first functional DEFAULTS
# ↓ else degradation: notice + nearest-lower topology, then a clear message
#
# The pin is read from the *environment* (callers load .env first), so this
# stays a pure function of (env, registry). Diagnostics + warnings go to stderr;
# only the resolved slug goes to stdout. Returns non-zero with a clear message
# when no functional default exists at any topology (never crashes).
model_default_target() {
local root="$1" model="$2" topology="$3"
# Compute the .env pin key for this model, then read its value from the
# environment (the caller has already loaded .env into the env).
local pin_key pin_value
pin_key="$(python3 - "$root" "$model" <<'PY_PINKEY'
import sys
from pathlib import Path
root = Path(sys.argv[1]); sys.path.insert(0, str(root))
from scripts.lib.profiles.compose_registry import model_default_pin_key # noqa: E402
print(model_default_pin_key(sys.argv[2]))
PY_PINKEY
)"
pin_value="${!pin_key:-}"
python3 - "$root" "$model" "$topology" "$pin_value" "$pin_key" <<'PY_MODEL_DEFAULT'
from __future__ import annotations
import sys
from pathlib import Path
root = Path(sys.argv[1])
sys.path.insert(0, str(root))
from scripts.lib.profiles.compose_registry import ( # noqa: E402
COMPOSE_REGISTRY,
FUNCTIONAL_STATUSES,
community_default_target,
curated_default_target,
model_of_slug,
slug_topology,
_nearest_lower_topology,
_topology_family,
)
model, topology, pin_value, pin_key = sys.argv[2:6]
def warn(msg: str) -> None:
print(f"[default] {msg}", file=sys.stderr)
family = _topology_family(topology)
# 1) User pin (.env). Validate: slug exists · its model matches the key · its
# topology matches the detected one · it is NOT (NA). Any failure → warn +
# fall through to the curated path (never block a launch — §6).
if pin_value:
entry = COMPOSE_REGISTRY.get(pin_value)
if entry is None:
warn(
f"pinned default {pin_value!r} ({pin_key}) is not a known slug — "
"ignoring the pin, using the curated default."
)
elif model_of_slug(pin_value) != model:
warn(
f"pinned default {pin_value!r} ({pin_key}) belongs to model "
f"{model_of_slug(pin_value)!r}, not {model!r} — ignoring the pin."
)
elif slug_topology(pin_value) != family:
warn(
f"pinned default {pin_value!r} ({pin_key}) is a "
f"{slug_topology(pin_value)} config but this rig is {family} — "
"ignoring the pin, using the curated default for this topology."
)
elif entry.get("status", "production") not in FUNCTIONAL_STATUSES:
warn(
f"pinned default {pin_value!r} ({pin_key}) is "
f"(NA: {entry.get('status')}) — not a reliable config; ignoring "
"the pin, using the curated default."
)
else:
print(pin_value)
raise SystemExit(0)
# 2) Community-ranked rung — defined now, returns None today (§13.4). Inserted
# between the user pin and the curated fallback.
community = community_default_target(model, family)
if community:
print(community)
raise SystemExit(0)
# 3) Curated fallback (§4) at the detected topology.
slug = curated_default_target(model, topology)
if slug:
print(slug)
raise SystemExit(0)
# 4) Degradation (§6): notice + nearest-lower topology, then a clear message.
fallback_topology = _nearest_lower_topology(topology)
while fallback_topology:
slug = curated_default_target(model, fallback_topology)
if slug:
warn(
f"no functional default for {model!r} on the detected "
f"{topology} topology — falling back to the {fallback_topology} "
f"default ({slug})."
)
print(slug)
raise SystemExit(0)
fallback_topology = _nearest_lower_topology(fallback_topology)
warn(
f"no default for {model!r} on this topology ({topology}) — pick a config "
"explicitly. Run: scripts/switch.sh --list"
)
raise SystemExit(1)
PY_MODEL_DEFAULT
}
# x_default_dispatch ROOT TOKEN TOPOLOGY MODEL → resolved slug on stdout.
#
# Parses an `X/default` token (design §13.1): if X is an engine name →
# engine-recommendation (registry_default_target on the given MODEL); else if X
# is a model-id → model_default_target (X overrides MODEL); else error. Both
# sets come from the registry and are disjoint. The caller passes the model to
# use for the engine-recommendation branch (its PRIMARY_MODEL / chosen model).
x_default_dispatch() {
local root="$1" token="$2" topology="$3" model="$4" x
x="${token%/default}"
local kind
kind="$(python3 - "$root" "$x" <<'PY_DISPATCH'
import sys
from pathlib import Path
root = Path(sys.argv[1]); sys.path.insert(0, str(root))
from scripts.lib.profiles.compose_registry import engine_set, model_set # noqa: E402
x = sys.argv[2]
if x in engine_set():
print("engine")
elif x in model_set():
print("model")
else:
print("unknown")
PY_DISPATCH
)"
case "$kind" in
engine)
registry_default_target "$root" "$model" "$x" "$topology"
;;
model)
model_default_target "$root" "$x" "$topology"
;;
*)
echo "[default] ERROR: '${token}': '${x}' is neither a known engine nor a known model." >&2
echo "[default] Engines: $(python3 -c "import sys; sys.path.insert(0,'$root'); from scripts.lib.profiles.compose_registry import engine_set; print(' '.join(sorted(engine_set())))")" >&2
echo "[default] Models: $(python3 -c "import sys; sys.path.insert(0,'$root'); from scripts.lib.profiles.compose_registry import model_set; print(' '.join(sorted(model_set())))")" >&2
return 1
;;
esac
}