briscoooe commited on
Commit
d32fcdc
·
verified ·
1 Parent(s): e61febb

eval: per-cube records for the replay page

Browse files
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 $? ==="