Files
esh-pfi-infrastructure/scripts/seat-inventory.py
T
vh 156e12619d feat(searxng): route search egress through the esh-scale SOCKS5 proxy
Committing work deployed on 2026-09-17 that had been left uncommitted, so
canonical intent stops disagreeing with the running host. The deployed
/opt/docker/conf/searxng/searxng-settings.yml is byte-identical to the
canonical file here, verified before this commit.

Search requests and their DNS now exit via socks5h://10.0.50.65:1080 on
esh-scale (CT 108), an application-level proxy rather than a host-wide exit
node; no route or firewall changes. microsocks runs as nobody under
searxng-egress.service, binds only 10.0.50.65:1080, and bypasses SOCKS auth
for source 10.100.50.40 alone — every other source must supply a password
regenerated at each start and never distributed. Verified active and enabled.

There is deliberately no direct-NH3 fallback: an ESH outage must fail the
search rather than silently revert egress.

⚠ THE CHANGE HAS NOT ACHIEVED ITS PURPOSE AS DEPLOYED. Two independent live
queries, 2026-09-18, both report brave "Suspended: too many requests",
duckduckgo "CAPTCHA" and startpage "Suspended: CAPTCHA", leaving google cse
as the only answering engine. Moving egress off NH3's residential address is
what this change did, and CAPTCHA avoidance was the stated reason searxng sits
at NH3 at all. The README anticipated the risk in its Dependency note; it has
materialised. Rollback procedure is in the README and the pre-change config is
kept on the host as searxng-settings.yml.pre-esh-20260917.

Measured egress also drifted from the value recorded at cutover: the README
notes 154.50.58.126, the proxy now exits 128.177.138.182. Expected — the README
pins no public IP and calls out WAN failover — but recorded here so the number
in the doc is not mistaken for current.

Also retargets seat-inventory.py's default host from the mesh address
100.64.0.7 to fv-ml1's LAN address 10.251.50.54, routed by the site gateway.
2026-09-18 12:30:42 -07:00

252 lines
11 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Regenerate the GPU seat inventory for an inference host from the LIVE box.
The point of this script is that the document it writes is DERIVED, never
hand-maintained. On 2026-09-13 the LiteLLM config was found describing `char-rp`
as a 31B model on a host and GPU it had not been on since August -- a hand-written
description that drifted silently for three weeks while looking authoritative.
Anything a human types here will drift the same way; anything read off the running
containers cannot.
scripts/seat-inventory.py # write the doc
scripts/seat-inventory.py --check # exit 1 if the committed doc is stale
scripts/seat-inventory.py --host fv-ml1 # another inference host
⚠ Reads state, changes nothing. Safe to run against production at any time.
"""
import argparse, json, re, subprocess, sys, datetime, pathlib
DEFAULT_HOST = "10.251.50.54" # fv-ml1 (LAN addr; routed by vb-gateway over the mesh)
DEFAULT_OUT = "docs/pfi/fv-ml1-gpu-seat-inventory.md"
GATEWAY = "10.250.50.70" # LiteLLM, for alias resolution
def ssh(host, cmd, sudo=False):
full = f"sudo -n {cmd}" if sudo else cmd
r = subprocess.run(
["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", f"infra-ops@{host}", full],
capture_output=True, text=True, timeout=120)
return r.stdout.strip()
def gather(host):
"""Everything comes from the box. No constants, no remembered values."""
seats, uuid2idx = {}, {}
for line in ssh(host, "nvidia-smi --query-gpu=index,uuid --format=csv,noheader").splitlines():
i, u = [x.strip() for x in line.split(",")]
uuid2idx[u] = i
# nvidia-smi reports the vLLM ENGINE CHILD pid; docker reports the CONTAINER
# pid. They are different numbers -- map through the cgroup, never directly.
for line in ssh(host, "nvidia-smi --query-compute-apps=gpu_uuid,pid,used_memory "
"--format=csv,noheader,nounits").splitlines():
u, pid, mem = [x.strip() for x in line.split(",")]
cg = ssh(host, f"cat /proc/{pid}/cgroup 2>/dev/null", sudo=True)
h = re.findall(r"[0-9a-f]{64}", cg)
if not h:
continue
name = ssh(host, f"docker inspect --format '{{{{.Name}}}}' {h[0]}", sudo=True).lstrip("/")
if not name:
continue
s = seats.setdefault(name, {"gpu": uuid2idx.get(u, "?"), "vram_mib": 0})
s["vram_mib"] += int(mem)
for name, s in seats.items():
try:
args = json.loads(ssh(host, f"docker inspect {name} --format '{{{{json .Args}}}}'", sudo=True))
except Exception:
args = []
def flag(f):
try:
i = args.index(f)
out = []
for x in args[i + 1:]:
if x.startswith("--"):
break
out.append(x)
return out
except ValueError:
return []
s["served"] = flag("--served-model-name")
s["ctx"] = (flag("--max-model-len") or ["-"])[0]
s["util"] = (flag("--gpu-memory-utilization") or ["-"])[0]
s["seqs"] = (flag("--max-num-seqs") or ["-"])[0]
s["quant"] = (flag("--quantization") or ["-"])[0]
s["spec"] = " ".join(flag("--speculative-config")) or "-"
s["image"] = ssh(host, f"docker inspect {name} --format '{{{{.Config.Image}}}}'", sudo=True)
s["model"] = next((a for a in args if a.startswith("/")), "?")
# resolve a bind-mounted /model to its real path, so lineage is traceable
mounts = ssh(host, f"docker inspect {name} --format "
f"'{{{{range .Mounts}}}}{{{{.Source}}}}|{{{{.Destination}}}};{{{{end}}}}'", sudo=True)
for m in mounts.split(";"):
if "|" in m:
src, dst = m.split("|", 1)
if dst in ("/model", "/local-models") and s["model"].startswith(dst):
s["model"] = s["model"].replace(dst, src, 1)
# weights + KV come from the engine's own startup log, not from arithmetic
logs = ssh(host, f"docker logs {name} 2>&1 | grep -aoE "
f"'model weights take [0-9.]+GiB|Model loading took [0-9.]+ GiB|"
f"GPU KV cache size: [0-9,]+ tokens' | sort -u", sudo=True)
w = re.search(r"([0-9.]+) ?GiB", logs)
t = re.search(r"([0-9,]+) tokens", logs)
s["weights_gib"] = w.group(1) if w else None
s["kv_tokens"] = int(t.group(1).replace(",", "")) if t else None
try:
s["concurrency"] = s["kv_tokens"] / int(s["ctx"])
except Exception:
s["concurrency"] = None
# lineage: .PROVENANCE.txt is a SIBLING of the model dir, not inside it
if s["model"].startswith("/tank"):
prov = ssh(host, f"head -6 {s['model']}.PROVENANCE.txt 2>/dev/null", sudo=True)
s["provenance"] = prov or None
cfg = ssh(host, f"""python3 -c "
import json
c=json.load(open('{s['model']}/config.json'))
t=c.get('text_config',c)
q=c.get('quantization_config') or {{}}
g=list((q.get('config_groups') or {{}}).values())
print(json.dumps({{
'arch': c.get('architectures'), 'type': c.get('model_type'),
'layers': t.get('num_hidden_layers'), 'experts': t.get('num_experts'),
'quant_method': q.get('quant_method'), 'quant_format': q.get('format'),
'groups': [{{'fmt':x.get('format'),
'w':(x.get('weights') or {{}}).get('num_bits'),
'a':((x.get('input_activations') or {{}}).get('num_bits'))}} for x in g],
}}))" 2>/dev/null""", sudo=True)
try:
s["config"] = json.loads(cfg)
except Exception:
s["config"] = None
return seats
def aliases():
out = ssh(GATEWAY, """python3 -c "
import yaml,json
d=yaml.safe_load(open('/opt/docker/conf/litellm/config.yaml'))
r=[]
for m in d.get('model_list',[]):
p=m.get('litellm_params',{})
ab=str(p.get('api_base',''))
if '10.251.50.54' in ab:
r.append([m.get('model_name'), ab.rsplit(':',1)[-1].split('/')[0]])
print(json.dumps(sorted(r)))" """, sudo=True)
try:
return json.loads(out)
except Exception:
return []
def render(seats, als, host):
stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
L = [
"# fv-ml1 — GPU seat inventory and model lineage",
"",
"<!-- GENERATED FILE — DO NOT EDIT BY HAND.",
" Regenerate: scripts/seat-inventory.py",
" Check drift: scripts/seat-inventory.py --check",
" Hand-edits are overwritten and, worse, drift silently while looking",
" authoritative — which is exactly the failure this file replaced. -->",
"",
f"**Generated {stamp}** by `scripts/seat-inventory.py`, read from the running",
f"containers on `{host}` — `docker inspect`, `nvidia-smi`, each model's own",
"`config.json`, and the `.PROVENANCE.txt` siblings on `/tank`.",
"",
"⚠ `.PROVENANCE.txt` lives *beside* the model directory, not inside it:",
"`/tank/aimodels/<model>.PROVENANCE.txt`. `ls <model>/` will not show it.",
"",
"## Placement, KV cache and concurrency",
"",
"| GPU | seat | VRAM | weights | KV tokens | ctx | concurrency | util |",
"|---|---|---|---|---|---|---|---|",
]
for n, s in sorted(seats.items(), key=lambda kv: (kv[1]["gpu"], -kv[1]["vram_mib"])):
conc = f"{s['concurrency']:.2f}×" if s["concurrency"] else "—"
L.append(f"| {s['gpu']} | `{n}` | {s['vram_mib']/1024:.1f} GiB | "
f"{s.get('weights_gib') or '—'} GiB | "
f"{s['kv_tokens']:,} | {s['ctx']} | **{conc}** | {s['util']} |"
if s["kv_tokens"] else
f"| {s['gpu']} | `{n}` | {s['vram_mib']/1024:.1f} GiB | "
f"{s.get('weights_gib') or '—'} GiB | — | {s['ctx']} | — | {s['util']} |")
L += ["", "**Concurrency** = KV tokens ÷ context: how many full-length requests fit at",
"once. Below ~1.0× the seat cannot hold even one conversation at its declared",
"context.", "", "## Lineage and quantization", ""]
for n, s in sorted(seats.items(), key=lambda kv: (kv[1]["gpu"], kv[0])):
L.append(f"### `{n}` — GPU {s['gpu']}")
L.append("")
L.append(f"- **serves:** {', '.join(f'`{x}`' for x in s['served']) or '—'}")
L.append(f"- **model:** `{s['model']}`")
c = s.get("config") or {}
if c:
arch = (c.get("arch") or ["?"])[0]
bits = f"{arch} ({c.get('type')}), {c.get('layers')} layers"
if c.get("experts"):
bits += f", {c['experts']} experts"
L.append(f"- **architecture:** {bits}")
if c.get("groups"):
gs = ", ".join(f"W{g['w']}A{g['a'] or 16} ({g['fmt']})" for g in c["groups"] if g.get("w"))
L.append(f"- **quantization:** {c.get('quant_method')} / {c.get('quant_format')} — {gs}")
if s["spec"] != "-":
L.append(f"- **speculative decoding:** `{s['spec']}`")
L.append(f"- **image:** `{s['image']}`"
+ (" ⚠ **floating tag**" if s["image"].endswith(":latest") else ""))
if s.get("provenance"):
L.append("- **provenance:**")
L += [" ```", *(" " + x for x in s["provenance"].splitlines()), " ```"]
L.append("")
dead = [a for a, p in als if p not in {str(x) for x in range(8000, 8100)}]
L += ["## Gateway aliases resolving to this host", "",
f"{len(als)} aliases. Ports with no listening seat are marked dead.", ""]
ports = {s["served"][0] if s["served"] else "": s for s in seats.values()}
L.append("| alias | port |")
L.append("|---|---|")
for a, p in als:
L.append(f"| `{a}` | {p} |")
L += ["", "---", "",
"*Lineage, provenance, model cards, measured tok/s and depth results live in the "
"hand-curated companion [`llm-seat-catalog.md`](llm-seat-catalog.md).*", "",
"*Regenerate with `scripts/seat-inventory.py` after ANY seat change —",
"model swap, quant change, context or utilization edit, or speculative-decoding",
"change. Run `--check` in CI to catch a stale document.*", ""]
return "\n".join(L)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default=DEFAULT_HOST)
ap.add_argument("--out", default=DEFAULT_OUT)
ap.add_argument("--check", action="store_true",
help="exit 1 if the committed document differs from the live box")
a = ap.parse_args()
doc = render(gather(a.host), aliases(), a.host)
p = pathlib.Path(a.out)
if a.check:
if not p.exists():
print(f"MISSING: {a.out}", file=sys.stderr)
return 1
# ignore the generation timestamp when comparing
strip = lambda t: "\n".join(l for l in t.splitlines() if not l.startswith("**Generated "))
if strip(p.read_text()) != strip(doc):
print(f"STALE: {a.out} does not match the live box. Run scripts/seat-inventory.py",
file=sys.stderr)
return 1
print(f"current: {a.out}")
return 0
p.write_text(doc)
print(f"wrote {a.out}")
return 0
if __name__ == "__main__":
sys.exit(main())