eval: per-cube records for the replay page
Browse files- code/eval_value_search.py +23 -1
- code/search.py +10 -1
- code/vast_bootstrap.sh +8 -1
- code/vast_onstart.sh +59 -0
code/eval_value_search.py
CHANGED
|
@@ -44,6 +44,12 @@ def main():
|
|
| 44 |
p.add_argument("--max-depth", type=int, default=26)
|
| 45 |
p.add_argument("--limit-per-depth", type=int, default=25)
|
| 46 |
p.add_argument("--depths", default="")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
args = p.parse_args()
|
| 48 |
|
| 49 |
import torch
|
|
@@ -72,6 +78,8 @@ def main():
|
|
| 72 |
seen[s["depth"]] += 1
|
| 73 |
states = kept
|
| 74 |
|
|
|
|
|
|
|
| 75 |
grid = [(b, l) for b in [int(x) for x in str(args.beam).split(",")]
|
| 76 |
for l in [float(x) for x in str(args.lam).split(",")]]
|
| 77 |
for beam, lam in grid:
|
|
@@ -81,9 +89,23 @@ def main():
|
|
| 81 |
flush=True)
|
| 82 |
for s_ in states:
|
| 83 |
t0 = time.time()
|
|
|
|
| 84 |
moves = solve_value_beam(model, s_["facelets"], device, beam=beam,
|
| 85 |
-
max_depth=args.max_depth, lam=lam)
|
| 86 |
ok = bool(moves) and apply_sequence(s_["facelets"], moves) == SOLVED
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
b = by_depth[s_["depth"]]
|
| 88 |
b[0] += ok
|
| 89 |
b[1] += 1
|
|
|
|
| 44 |
p.add_argument("--max-depth", type=int, default=26)
|
| 45 |
p.add_argument("--limit-per-depth", type=int, default=25)
|
| 46 |
p.add_argument("--depths", default="")
|
| 47 |
+
p.add_argument("--records", default="",
|
| 48 |
+
help="Also write one JSON line per cube (depth, index, beam, "
|
| 49 |
+
"moves, solved, seconds, states scored) to this path, and "
|
| 50 |
+
"print each as a RECORD line. The totals alone cannot be "
|
| 51 |
+
"uploaded: they do not say which cube failed or with what "
|
| 52 |
+
"moves, and the first value-iteration sweep kept only them.")
|
| 53 |
args = p.parse_args()
|
| 54 |
|
| 55 |
import torch
|
|
|
|
| 78 |
seen[s["depth"]] += 1
|
| 79 |
states = kept
|
| 80 |
|
| 81 |
+
rec_fh = open(args.records, "a") if args.records else None
|
| 82 |
+
|
| 83 |
grid = [(b, l) for b in [int(x) for x in str(args.beam).split(",")]
|
| 84 |
for l in [float(x) for x in str(args.lam).split(",")]]
|
| 85 |
for beam, lam in grid:
|
|
|
|
| 89 |
flush=True)
|
| 90 |
for s_ in states:
|
| 91 |
t0 = time.time()
|
| 92 |
+
stats = {}
|
| 93 |
moves = solve_value_beam(model, s_["facelets"], device, beam=beam,
|
| 94 |
+
max_depth=args.max_depth, lam=lam, stats=stats)
|
| 95 |
ok = bool(moves) and apply_sequence(s_["facelets"], moves) == SOLVED
|
| 96 |
+
if rec_fh:
|
| 97 |
+
rec = {"depth": s_["depth"], "index": s_["index"],
|
| 98 |
+
"facelets": s_["facelets"], "beam": beam, "lam": lam,
|
| 99 |
+
"max_depth": args.max_depth, "moves": moves, "solved": ok,
|
| 100 |
+
"seconds": round(time.time() - t0, 3),
|
| 101 |
+
"scored": stats.get("scored"), "steps": stats.get("steps"),
|
| 102 |
+
"device": torch.cuda.get_device_name(0) if device == "cuda" else "cpu",
|
| 103 |
+
# Fixed per cube, so the upload derives the same row id
|
| 104 |
+
# from the same record however many times it is re-run.
|
| 105 |
+
"finished_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
|
| 106 |
+
rec_fh.write(json.dumps(rec) + "\n")
|
| 107 |
+
rec_fh.flush()
|
| 108 |
+
print("RECORD " + json.dumps(rec), flush=True)
|
| 109 |
b = by_depth[s_["depth"]]
|
| 110 |
b[0] += ok
|
| 111 |
b[1] += 1
|
code/search.py
CHANGED
|
@@ -89,13 +89,19 @@ def heuristic(model, states, device, batch_size=1024):
|
|
| 89 |
|
| 90 |
|
| 91 |
def solve_value_beam(model, state, device, beam=64, max_depth=26, lam=0.0,
|
| 92 |
-
batch_size=1024):
|
| 93 |
"""Beam search over states, ranked by `h + lam * g`.
|
| 94 |
|
| 95 |
Returns the move list that solves, or [] if the beam is exhausted. Visited
|
| 96 |
states are tracked globally: revisiting one cannot help, and on a group this
|
| 97 |
symmetric the frontier collapses onto duplicates quickly without it.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
"""
|
|
|
|
|
|
|
| 99 |
if state == SOLVED:
|
| 100 |
return []
|
| 101 |
frontier = [(state, [])]
|
|
@@ -116,6 +122,9 @@ def solve_value_beam(model, state, device, beam=64, max_depth=26, lam=0.0,
|
|
| 116 |
if not children:
|
| 117 |
return []
|
| 118 |
h = heuristic(model, children, device, batch_size)
|
|
|
|
|
|
|
|
|
|
| 119 |
scored = sorted(zip(h, children, paths), key=lambda x: x[0] + lam * len(x[2]))
|
| 120 |
frontier = [(c, p) for _, c, p in scored[:beam]]
|
| 121 |
return []
|
|
|
|
| 89 |
|
| 90 |
|
| 91 |
def solve_value_beam(model, state, device, beam=64, max_depth=26, lam=0.0,
|
| 92 |
+
batch_size=1024, stats=None):
|
| 93 |
"""Beam search over states, ranked by `h + lam * g`.
|
| 94 |
|
| 95 |
Returns the move list that solves, or [] if the beam is exhausted. Visited
|
| 96 |
states are tracked globally: revisiting one cannot help, and on a group this
|
| 97 |
symmetric the frontier collapses onto duplicates quickly without it.
|
| 98 |
+
|
| 99 |
+
`stats`, when given a dict, receives `scored` (states the network evaluated)
|
| 100 |
+
and `steps` (levels expanded) -- the cost of the solve, which a replay page
|
| 101 |
+
can show beside the time.
|
| 102 |
"""
|
| 103 |
+
if stats is not None:
|
| 104 |
+
stats["scored"], stats["steps"] = 0, 0
|
| 105 |
if state == SOLVED:
|
| 106 |
return []
|
| 107 |
frontier = [(state, [])]
|
|
|
|
| 122 |
if not children:
|
| 123 |
return []
|
| 124 |
h = heuristic(model, children, device, batch_size)
|
| 125 |
+
if stats is not None:
|
| 126 |
+
stats["scored"] += len(children)
|
| 127 |
+
stats["steps"] += 1
|
| 128 |
scored = sorted(zip(h, children, paths), key=lambda x: x[0] + lam * len(x[2]))
|
| 129 |
frontier = [(c, p) for _, c, p in scored[:beam]]
|
| 130 |
return []
|
code/vast_bootstrap.sh
CHANGED
|
@@ -92,7 +92,14 @@ if [ -n "${EVAL_VALUE_ONLY:-}" ]; then
|
|
| 92 |
/opt/venv/bin/python eval_value_search.py --value "$EVAL_VALUE_ONLY" \
|
| 93 |
--value-file "${VALUE_FILE:-value.pt}" \
|
| 94 |
--beam "${SEARCH_BEAM:-64}" --limit-per-depth "${SEARCH_N:-25}" \
|
| 95 |
-
${SEARCH_DEPTHS:+--depths "$SEARCH_DEPTHS"} ${SEARCH_LAM:+--lam "$SEARCH_LAM"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
echo "=== done ==="
|
| 97 |
exit 0
|
| 98 |
fi
|
|
|
|
| 92 |
/opt/venv/bin/python eval_value_search.py --value "$EVAL_VALUE_ONLY" \
|
| 93 |
--value-file "${VALUE_FILE:-value.pt}" \
|
| 94 |
--beam "${SEARCH_BEAM:-64}" --limit-per-depth "${SEARCH_N:-25}" \
|
| 95 |
+
${SEARCH_DEPTHS:+--depths "$SEARCH_DEPTHS"} ${SEARCH_LAM:+--lam "$SEARCH_LAM"} \
|
| 96 |
+
${SEARCH_RECORDS:+--records "$SEARCH_RECORDS"}
|
| 97 |
+
# The per-cube records are the product of the run, so they leave the box before
|
| 98 |
+
# it can be destroyed. The RECORD lines in the log are the fallback copy.
|
| 99 |
+
if [ -n "${SEARCH_RECORDS:-}" ] && [ -n "${RECORDS_REPO:-}" ]; then
|
| 100 |
+
echo "=== pushing records to ${RECORDS_REPO} ==="
|
| 101 |
+
RR="$RECORDS_REPO" RF="$SEARCH_RECORDS" /opt/venv/bin/python -c 'import os; from huggingface_hub import HfApi; HfApi(token=os.environ["HF_TOKEN"]).upload_file(path_or_fileobj=os.environ["RF"], path_in_repo="eval/" + os.path.basename(os.environ["RF"]), repo_id=os.environ["RR"])'
|
| 102 |
+
fi
|
| 103 |
echo "=== done ==="
|
| 104 |
exit 0
|
| 105 |
fi
|
code/vast_onstart.sh
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# What a rented box runs first. Give it to vast.ai as the instance's `onstart`
|
| 3 |
+
# script, with `runtype: "ssh"` and a vastai/base-image, then let it fetch the
|
| 4 |
+
# code and hand over to vast_bootstrap.sh.
|
| 5 |
+
#
|
| 6 |
+
# Two things this file exists to stop repeating.
|
| 7 |
+
#
|
| 8 |
+
# **`runtype: "args"` with an onstart script does not work.** vast hands the
|
| 9 |
+
# script text to the container as its command, runc tries to exec it, and the
|
| 10 |
+
# instance dies with "unable to start container process: exec:
|
| 11 |
+
# #!/bin/bash\nexec > ...". A plain nvidia/cuda image has no vast entrypoint to
|
| 12 |
+
# run an onstart script at all. Use a vastai/base-image and runtype "ssh"; the
|
| 13 |
+
# box then runs this file for you.
|
| 14 |
+
#
|
| 15 |
+
# **Never let the shell trace expand HF_TOKEN.** `set -x` prints every expanded
|
| 16 |
+
# word, so a bare `[ -z "$HF_TOKEN" ]` writes the token into the instance log in
|
| 17 |
+
# clear text -- which is then readable through vast's log API and copied into
|
| 18 |
+
# every archive made from it. That happened on 2026-09-16 and cost a token
|
| 19 |
+
# rotation. Tracing goes off around anything that touches a secret.
|
| 20 |
+
exec > >(tee -a /workspace/onstart.log) 2>&1
|
| 21 |
+
set -x
|
| 22 |
+
# vast writes API-supplied env vars here; onstart does not always inherit them.
|
| 23 |
+
source /etc/environment 2>/dev/null || true
|
| 24 |
+
|
| 25 |
+
set +x # <-- nothing between here and `set -x` may reach the log
|
| 26 |
+
if [ -z "${HF_TOKEN:-}" ]; then
|
| 27 |
+
echo "ABORT: HF_TOKEN not in environment"
|
| 28 |
+
exit 1
|
| 29 |
+
fi
|
| 30 |
+
set -x
|
| 31 |
+
|
| 32 |
+
export DEBIAN_FRONTEND=noninteractive
|
| 33 |
+
apt-get update -qq
|
| 34 |
+
apt-get install -y -qq python3-pip python3-venv python3-dev build-essential curl git
|
| 35 |
+
pip3 install --break-system-packages -q huggingface_hub
|
| 36 |
+
|
| 37 |
+
# The repo is private, so a rented box cannot clone it. Code ships through the
|
| 38 |
+
# model's own Hub repo instead, reusing the token the checkpoints already need.
|
| 39 |
+
python3 - <<'PY'
|
| 40 |
+
import os
|
| 41 |
+
from huggingface_hub import snapshot_download
|
| 42 |
+
print(snapshot_download(os.environ.get("CODE_REPO", "briscoooe/tiny-cube-value-iter"),
|
| 43 |
+
allow_patterns=["code/*"], token=os.environ["HF_TOKEN"],
|
| 44 |
+
local_dir="/workspace/hub"))
|
| 45 |
+
PY
|
| 46 |
+
|
| 47 |
+
# snapshot_download does not raise when the box cannot reach Hugging Face -- it
|
| 48 |
+
# returns an empty local dir, so the run would exit 0 having done nothing. Check
|
| 49 |
+
# for a file from the download, loudly.
|
| 50 |
+
if [ ! -f /workspace/hub/code/vast_bootstrap.sh ]; then
|
| 51 |
+
echo "ABORT: vast_bootstrap.sh missing after download; this box cannot reach Hugging Face"
|
| 52 |
+
exit 1
|
| 53 |
+
fi
|
| 54 |
+
|
| 55 |
+
cp -r /workspace/hub/code /workspace/code
|
| 56 |
+
cd /workspace/code
|
| 57 |
+
echo "=== starting bootstrap ==="
|
| 58 |
+
bash vast_bootstrap.sh
|
| 59 |
+
echo "=== BOOTSTRAP EXITED $? ==="
|