GPU fleet: RunPod client, the phase-1 collector, the phase-2 launcher (node restarted with the proof verifier), parallel status, the toolchain fix

This commit is contained in:
igneum-labs 2026-10-06 12:14:57 +00:00
parent 3fd25fc40d
commit c0d85ee9ec
5 changed files with 147 additions and 9 deletions

22
tools/fleet/box-prover.sh Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Phase 2 launcher: restarts the box's node with IGNEUM_PROOF_VERIFIER (so its pool verifies records and its templates
# carry them, the proving agent's note of 6 October 2026), waits for it to be synced again (the data dir is kept, so
# seconds), then runs box-prover.py with the card's profile: THRESHOLD and MINER from the card's memory unless given.
set -uo pipefail
F=/root/fleet; OUT=$F/out; B=/opt/igneum/pkg/bin; FLOOR=/opt/igneum-floor; HOST=$FLOOR/bin/igneum-prove-host
mkdir -p $OUT; exec >> $OUT/prover-launch.log 2>&1
stamp() { date -u +%Y-%m-%dT%H:%M:%SZ; }
TOTAL="$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits -i 0 | tr -d ' ')"
if [ -z "${THRESHOLD:-}" ]; then
if [ "$TOTAL" -ge 23000 ]; then THRESHOLD=""; elif [ "$TOTAL" -ge 15000 ]; then THRESHOLD=134217728; else THRESHOLD=67108864; fi
fi
if [ -z "${MINER:-}" ]; then if [ "$TOTAL" -ge 15000 ]; then MINER=keep; else MINER=pause; fi; fi
echo "RESULT launch $(stamp) total_mib=$TOTAL threshold=${THRESHOLD:-default} miner=$MINER"
pkill -f "igneum-miner mine" 2>/dev/null; pkill -f igneum-worker-cuda 2>/dev/null; pkill -f sp1-gpu-server 2>/dev/null; rm -f /tmp/sp1-cuda-*.sock
pkill -x igneumd; sleep 4; pkill -9 -x igneumd 2>/dev/null; sleep 1
IGNEUM_PROOF_VERIFIER=$HOST nohup $B/igneumd --devnet --appdir=$F/node --rpclisten=127.0.0.1:26610 --evm-rpclisten=127.0.0.1:26790 --listen=0.0.0.0:26611 \
--addpeer=188.245.5.161:26611 --override-params-file=$F/override.json --nodnsseed --disable-upnp --nologfiles --yes >> $F/node.log 2>&1 &
sleep 10
echo "RESULT node_restarted $(stamp) pid=$(pgrep -x igneumd | head -1) verifier=$(grep -c 'IGNEUM_PROOF_VERIFIER\|proof verifier' $F/node.log)"
export THRESHOLD MINER
exec python3 -u $F/in/box-prover.py

View file

@ -56,7 +56,7 @@ fi
echo "RESULT node pid=$(pgrep -x igneumd | head -1) digest=$(grep -o 'digest: [0-9a-f]*' $F/node.log | head -1 | awk '{print $2}') fresh=$(grep -c 'fresh-record rule' $F/node.log)"
# 3. rust and go
export PATH="$HOME/.cargo/bin:/opt/igneum-floor/go/bin:$PATH"
export PATH="$HOME/.cargo/bin:/opt/igneum-floor/go/bin:$PATH" RUSTUP_TOOLCHAIN=stable # the SP1 repo's rust-toolchain.toml would pull llvm-tools, rustc-dev and clippy from a CDN some hosts reach badly (the 3060 box, 12:00Z); plain stable builds the server
if [ ! -x "$HOME/.cargo/bin/cargo" ]; then say "rustup"; curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal >/dev/null 2>&1 || fail rustup; fi
if [ ! -x /opt/igneum-floor/go/bin/go ]; then
say "go"

53
tools/fleet/collect.py Executable file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Phase 1 collector: reads ~/Desktop/fleet/<instance>/matrix.json (and ember.json when present) for every phase-1 box,
writes ~/Desktop/fleet/results.json (the fleet page's rows, keyed by card) and prints the markdown table for
docs/analysis/prover-tiers-real-cards.md. The verdict rule, per card (the 9.0 GB mine-and-prove line and the tier
gates of docs/analysis/prover-floor.md), on the card's OWN numbers:
proves alone = a compressed proof of the v1 shard at 2^26 or 2^27 verified with the server's own working set
plus the idle under the card's memory, but mine-and-prove over the line
mines and proves = the compressed v1 shard beside the miner verified (peak under the card's memory)
mines and proves core-only = only the core-only point beside the miner fits
mining only = no proof point verified on the card (every point refused or out of memory)
"""
import json, os, sys, glob
ROOT = os.path.expanduser("~/Desktop/fleet")
reg = json.load(open(f"{ROOT}/boxes.json"))
def gb(mib): return round(mib / 1024, 1)
rows = {}; table = []
for iid, b in reg.items():
if not b.get("phase") == "1": continue
mp = f"{ROOT}/{iid}/matrix.json"
if not os.path.exists(mp): continue
m = json.load(open(mp)); pts = {r["name"]: r for r in m["rows"] if r.get("row") == "point"}; miner = next((r for r in m["rows"] if r.get("row") == "miner"), {})
total = m["total_mib"]; idle = m["idle_mib"]
def ok(n): p = pts.get(n); return p and p.get("verified") == "yes"
def own(n): p = pts.get(n); return p.get("own_mib") if p else None
def secs(n): p = pts.get(n); return p.get("prove_s") if p else None
alone = next((n for n in ("alone-comp-26-v1", "alone-comp-27-v1", "alone-comp-25-v1") if ok(n)), None)
beside = next((n for n in ("miner-comp-26-v1", "miner-comp-27-v1") if ok(n)), None)
core_b = next((n for n in ("miner-core-25-v1", "miner-core-26-v1", "miner-core-24-v1") if ok(n)), None)
stock = pts.get("stock-comp-v1", {})
if beside: verdict = "mines and proves"
elif alone and core_b: verdict = "mines and proves core-only"
elif alone: verdict = "proves alone"
else: verdict = "mining only"
card = m["card"].replace("NVIDIA GeForce ", "").replace("NVIDIA ", "")
r = {"card": card, "vram_gb": round(total / 1024), "mhs": miner.get("mhs"), "watts": miner.get("watts"), "miner_gb": gb(miner.get("own_mib", 0)),
"stock": "refused: " + (stock.get("err") or "")[:60] if stock.get("verified") != "yes" else f"proved {stock.get('prove_s')} s at {gb(stock.get('peak_mib', 0))} GB",
"prove_alone_gb": gb(own(alone)) if alone else None, "shard_s": secs(alone), "prove_alone_point": alone,
"mine_prove_gb": gb(pts[beside]["peak_mib"]) if beside else None, "mine_prove_s": secs(beside), "core_only_gb": gb(own(core_b)) if core_b else None, "core_only_s": secs(core_b),
"verdict": verdict, "points": {n: {"v": p.get("verified"), "own": p.get("own_mib"), "peak": p.get("peak_mib"), "s": p.get("prove_s"), "err": (p.get("err") or "")[:80]} for n, p in pts.items()}}
r["tier_line"] = {"mines and proves": f"{card} ({r['vram_gb']} GB): mines and proves; the v1 shard beside the miner {r['mine_prove_s']} s at {r['mine_prove_gb']} GB peak",
"mines and proves core-only": f"{card} ({r['vram_gb']} GB): proves alone ({r['shard_s']} s) and mines beside a core-only prover ({r['core_only_gb']} GB own)",
"proves alone": f"{card} ({r['vram_gb']} GB): proves alone, {r['shard_s']} s a v1 shard at {r['prove_alone_gb']} GB own; not beside the miner",
"mining only": f"{card} ({r['vram_gb']} GB): mines only ({r['mhs']} MH/s); no proof point fits"}[verdict]
ep = f"{ROOT}/{iid}/ember.json"
if os.path.exists(ep):
e = json.load(open(ep)); c = e["chosen"]
r.update({"tune_w": c["watts"], "tune_clock_mhz": c["clock_mhz"], "tune_mhs": c["mhs"], "tune_mhw": c["eff"], "ladder": [{"w": s["watts"], "mhs": s["mhs"], "mhw": s["eff"], "step": s["label"]} for s in e["steps"]], "tune_plan": e["plan"]})
rows[card] = r
table.append(f"| {card} | {r['vram_gb']} | {idle} | {r['mhs']} MH/s at {r['watts']} W, {r['miner_gb']} GB | {r['stock']} | " +
f"{r['prove_alone_gb'] or 'no'} GB, {r['shard_s'] or ''} s ({alone or 'none'}) | {r['mine_prove_gb'] or 'no'} GB peak, {r['mine_prove_s'] or ''} s | {r['core_only_gb'] or 'no'} GB, {r['core_only_s'] or ''} s | {verdict} |")
json.dump(rows, open(f"{ROOT}/results.json", "w"), indent=1)
print("| Card | VRAM GB | Idle MiB | Miner | Stock SP1 6.8.1 | Patched, proves alone (own) | Beside the miner (peak) | Core-only beside the miner (own) | Verdict |\n|---|---|---|---|---|---|---|---|---|")
print("\n".join(sorted(table)))

View file

@ -84,20 +84,28 @@ def setup(labels):
if not b.get("ssh_ok"): print(b["label"], "no ssh yet"); continue
ssh(b, "mkdir -p /root/fleet/in /root/fleet/out")
if scp(b, INPUTS + [os.path.join(HERE, "box-setup.sh")], "/root/fleet/in/") != 0: print(b["label"], "scp failed"); continue
rc, out, err = ssh(b, f"cd /root/fleet && chmod +x in/box-setup.sh && if pgrep -f in/box-setup.sh >/dev/null; then echo already; else ARCHS={b['archs']} LABEL={b['label']} WALLET={b['wallet']} setsid nohup in/box-setup.sh </dev/null >/dev/null 2>&1 & echo started; fi", timeout=60)
rc, out, err = ssh(b, f"cd /root/fleet && chmod +x in/box-setup.sh && if pgrep -f '^bash in/box-setup.sh' >/dev/null; then echo already; else RUSTUP_TOOLCHAIN=stable ARCHS={b['archs']} LABEL={b['label']} WALLET={b['wallet']} setsid nohup in/box-setup.sh </dev/null >/dev/null 2>&1 & echo started; fi", timeout=60)
print(b["label"], out.strip()[:40])
b["setup_started"] = now(); print(b["label"], "setup started")
save(load() | {k: v for k, v in boxes(labels).items()})
def status(labels):
reg = load()
for iid, b in boxes(labels, reg).items():
if not b.get("ssh_ok"): print(f"{b['label']:<14} {iid} no ssh"); continue
try:
rc, out, err = ssh(b, "grep -E '^(RESULT|STAGE)' /root/fleet/setup.log 2>/dev/null | tail -1; grep -c '^RESULT setup_done' /root/fleet/setup.log 2>/dev/null; /opt/igneum/pkg/bin/igneum-miner watch 1 grpc://127.0.0.1:26610 2>/dev/null | grep -o 'blocks=[0-9]*.*synced=[a-z]*' | tail -1; grep -E '^(RESULT|STAGE)' /root/fleet/out/matrix.log 2>/dev/null | tail -1", timeout=60)
lines = out.strip().split("\n"); print(f"{b['label']:<14} {iid} | " + " | ".join(l[:110] for l in lines))
if any(l.startswith("1") for l in lines[1:2]) and b.get("state") == "installing": b["state"] = "running"; save(reg)
except subprocess.TimeoutExpired: print(f"{b['label']:<14} {iid} ssh timeout")
from concurrent.futures import ThreadPoolExecutor
def one(item):
iid, b = item
if not b.get("ssh_ok"): return f"{b['label']:<14} {iid} no ssh", None
rc, out, err = ssh(b, "grep -E '^(RESULT|STAGE)' /root/fleet/setup.log 2>/dev/null | tail -1; grep -c '^RESULT setup_done' /root/fleet/setup.log 2>/dev/null; /opt/igneum/pkg/bin/igneum-miner watch 1 grpc://127.0.0.1:26610 2>/dev/null | grep -o 'blocks=[0-9]*.*synced=[a-z]*' | tail -1; grep -E '^(RESULT|STAGE)' /root/fleet/out/matrix.log 2>/dev/null | tail -1; grep -E '^RESULT' /root/fleet/out/ember.log 2>/dev/null | tail -1; grep -E '^RESULT' /root/fleet/out/prover.log 2>/dev/null | tail -1", timeout=45)
if rc == 124: return f"{b['label']:<14} {iid} ssh timeout", None
lines = out.strip().split("\n")
done = len(lines) > 1 and lines[1].strip() == "1"
return f"{b['label']:<14} {iid} | " + " | ".join(l[:120] for l in lines if l.strip() and l.strip() not in ("0", "1")), done
items = list(boxes(labels, reg).items())
with ThreadPoolExecutor(max_workers=16) as ex: results = list(ex.map(one, items))
for (iid, b), (line, done) in zip(items, results):
print(line)
if done and b.get("state") == "installing": b["state"] = "running"
save(reg)
def run(script, labels, env=""):
for iid, b in boxes(labels).items():

55
tools/fleet/runpod.py Executable file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""RunPod REST client for the fleet (https://rest.runpod.io/v1, Bearer from ~/.config/runpod/credentials; the GraphQL
endpoint refuses the Bearer header, so nothing here touches account settings: the fleet key goes in per pod through
the PUBLIC_KEY env, which RunPod's images write to authorized_keys). Ledger lines as vast.py.
runpod.py gpus the GPU types with availability and prices
runpod.py rent <gpuTypeId> <label> [--count 8] [--disk 80] [--image ...] [--secure]
runpod.py list pods: id, name, status, ssh endpoint
runpod.py destroy <pod_id>...
"""
import json, os, sys, time, urllib.request, urllib.error, datetime, argparse
KEY = open(os.path.expanduser("~/.config/runpod/credentials")).read().strip()
BASE = "https://rest.runpod.io/v1"
LEDGER = os.path.expanduser("~/Desktop/fleet/ledger.jsonl")
IMAGE = "runpod/pytorch:2.8.0-py3.11-cuda12.8.1-cudnn-devel-ubuntu22.04"
PUB = open(os.path.expanduser("~/.ssh/igneum-fleet.pub")).read().strip()
def call(method, path, body=None):
req = urllib.request.Request(BASE + path, data=(json.dumps(body).encode() if body is not None else None),
headers={"Authorization": "Bearer " + KEY, "Content-Type": "application/json"}, method=method)
try:
with urllib.request.urlopen(req, timeout=90) as r:
t = r.read(); return json.loads(t) if t else {}
except urllib.error.HTTPError as e: raise SystemExit(f"{method} {path}: HTTP {e.code}: {e.read().decode(errors='replace')[:400]}")
def now(): return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def ledger(row):
with open(LEDGER, "a") as f: f.write(json.dumps(row) + "\n")
def gpus():
for g in call("GET", "/gputypes"):
print(f"{g.get('id'):<34} {g.get('displayName','')!s:<22} {g.get('memoryInGb')}GB secure ${g.get('securePrice')}/h community ${g.get('communityPrice')}/h max {g.get('maxGpuCount')} avail {g.get('lowestPrice', {}) if isinstance(g.get('lowestPrice'), dict) else ''}")
def rent(gpu, label, count=8, disk=80, image=IMAGE, secure=False):
body = {"name": label, "imageName": image, "gpuTypeIds": [gpu], "gpuCount": count, "containerDiskInGb": disk, "volumeInGb": 0,
"cloudType": "SECURE" if secure else "COMMUNITY", "ports": ["22/tcp"], "env": {"PUBLIC_KEY": PUB}, "supportPublicIp": True, "computeType": "GPU"}
d = call("POST", "/pods", body)
row = {"t": now(), "event": "rent", "provider": "runpod", "instance": d.get("id"), "label": label, "gpu": gpu, "num_gpus": count, "dph": d.get("costPerHr"), "disk": disk}
ledger(row); print(json.dumps(row)); print(json.dumps(d)[:600]); return d
def pods():
out = call("GET", "/pods")
return out if isinstance(out, list) else out.get("pods", [])
def ssh_of(p):
for m in (p.get("portMappings") or {}).items() if isinstance(p.get("portMappings"), dict) else []:
if str(m[0]) == "22": return p.get("publicIp"), m[1]
return p.get("publicIp"), None
def destroy(pid):
d = call("DELETE", f"/pods/{pid}"); ledger({"t": now(), "event": "destroy", "provider": "runpod", "instance": pid}); print("destroyed", pid, d)
if __name__ == "__main__":
a = sys.argv[1:]
if a[0] == "gpus": gpus()
elif a[0] == "rent":
ap = argparse.ArgumentParser(); ap.add_argument("gpu"); ap.add_argument("label"); ap.add_argument("--count", type=int, default=8); ap.add_argument("--disk", type=int, default=80); ap.add_argument("--image", default=IMAGE); ap.add_argument("--secure", action="store_true")
n = ap.parse_args(a[1:]); rent(n.gpu, n.label, n.count, n.disk, n.image, n.secure)
elif a[0] == "list":
for p in pods(): print(p.get("id"), p.get("name"), p.get("desiredStatus"), p.get("gpuCount"), p.get("machine", {}).get("gpuTypeId") if isinstance(p.get("machine"), dict) else "", f"${p.get('costPerHr')}/h", ssh_of(p), (p.get("portMappings")))
elif a[0] == "destroy":
for i in a[1:]: destroy(i)
elif a[0] == "raw": print(json.dumps(pods(), indent=1)[:3000])