#!/usr/bin/env bash # Provisions a vast.ai instance and runs the full pipeline: generate -> verify -> # train -> push to the Hub. Written to be re-runnable: a reclaimed instance can be # replaced and this script started again from scratch. # # Expects, in the environment: # HF_TOKEN write token, for checkpoint pushes # HUB_REPO e.g. yourname/tiny-cube-solver # Optional: PAIRS (default 10000000), STEPS, HIDDEN, LAYERS set -euo pipefail PAIRS="${PAIRS:-10000000}" STEPS="${STEPS:-40000}" HIDDEN="${HIDDEN:-384}" LAYERS="${LAYERS:-8}" CORES="$(nproc)" echo "=== $CORES cores, $(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || echo 'no GPU') ===" # Build against SYSTEM python, not the image's conda python. kociemba ships a C # extension and a pure-Python fallback, and it falls back *silently* when the # extension cannot build -- which is what happens if the venv's interpreter has no # matching dev headers. python3-dev is therefore not optional here. apt-get update -qq # libffi-dev is the one that actually matters and is the least obvious: kociemba's # native extension is built through cffi, which needs ffi.h. Without it the # compile dies with "fatal error: ffi.h: No such file or directory", kociemba # installs its pure-Python fallback instead, and pip still exits 0. apt-get install -y -qq build-essential python3-dev python3-venv libffi-dev git # Prefer python3.11. kociemba 1.2.1 builds its C extension cleanly on 3.11 (the # version this repo is developed against) but silently produces no extension on # the 3.10 that ships as system python in the Ubuntu 22.04 based images -- pip # still reports success, so the only symptom is the fallback. The PyTorch images # carry a conda 3.11, so prefer that and fall back to system python. PYBIN="" for cand in /opt/conda/bin/python3.11 /opt/conda/bin/python "$(command -v python3.11 || true)" /usr/bin/python3; do [ -x "$cand" ] || continue if "$cand" -c 'import sys; sys.exit(0 if sys.version_info[:2] >= (3,11) else 1)' 2>/dev/null; then PYBIN="$cand"; break fi done [ -z "$PYBIN" ] && PYBIN=/usr/bin/python3 echo "building venv from $PYBIN ($($PYBIN -V 2>&1))" "$PYBIN" -m venv /opt/venv /opt/venv/bin/pip install -q --upgrade pip # kociemba's setup.py predates modern setuptools' removal of install_layout. /opt/venv/bin/pip install -q "setuptools<60" wheel # Full output, not a tail: when the C extension fails to build, kociemba still # installs successfully (as pure Python), so the compiler error is the only # evidence of what went wrong and it must not be truncated away. /opt/venv/bin/pip install --no-build-isolation kociemba 2>&1 | tail -40 # Last resort before giving up: build from the upstream git source, whose build # metadata is newer than the 2017 PyPI sdist. if ! /opt/venv/bin/python -c "import kociemba.ckociembawrapper" 2>/dev/null; then echo "=== sdist build produced no extension; trying upstream git ===" /opt/venv/bin/pip install --force-reinstall --no-build-isolation \ "git+https://github.com/muodov/kociemba.git" 2>&1 | tail -15 || true fi if ! /opt/venv/bin/python -c "import kociemba.ckociembawrapper" 2>/dev/null; then echo "=== NATIVE BUILD FAILED -- diagnostics ===" echo "python: $(/opt/venv/bin/python -V 2>&1) at $(/opt/venv/bin/python -c 'import sys;print(sys.executable)')" echo "gcc: $(gcc --version 2>&1 | head -1)" echo "includes: $(/opt/venv/bin/python -c 'import sysconfig;print(sysconfig.get_paths()["include"])')" ls -l "$(/opt/venv/bin/python -c 'import sysconfig;print(sysconfig.get_paths()["include"])')/Python.h" 2>&1 || echo " Python.h MISSING" dpkg -l | grep -E "python3.*dev|build-essential" || echo " no dev packages found" echo "=== retrying verbosely ===" /opt/venv/bin/pip install --force-reinstall --no-build-isolation -v kociemba 2>&1 | grep -iE "error|gcc|Python.h|fatal" | head -30 fi /opt/venv/bin/pip install -q torch --index-url https://download.pytorch.org/whl/cu128 /opt/venv/bin/pip install -q transformers huggingface_hub # Search evaluation only: skip data generation and training entirely, download a # trained value model and score the state search on the canonical scrambles. The # search needs a GPU -- each solve evaluates beam*18 children per step -- so it # cannot be done from the session container. if [ -n "${EVAL_VALUE_ONLY:-}" ]; then cd "${CODE_DIR:-/workspace/code}" echo "=== evaluating value search from ${EVAL_VALUE_ONLY} ===" /opt/venv/bin/python eval_value_search.py --value "$EVAL_VALUE_ONLY" \ --beam "${SEARCH_BEAM:-64}" --limit-per-depth "${SEARCH_N:-25}" \ ${SEARCH_DEPTHS:+--depths "$SEARCH_DEPTHS"} ${SEARCH_LAM:+--lam "$SEARCH_LAM"} echo "=== done ===" exit 0 fi # Hard gate. The pure-Python fallback is ~50x slower, so generating this dataset # with it would take days rather than hours -- and the only symptom is a warning # buried in the log while the meter runs. Fail here instead. echo "=== verifying native kociemba ===" /opt/venv/bin/python - <<'PYCHECK' import sys, time import kociemba from kociemba import ckociembawrapper # absent entirely when the C build failed SOLVED = "".join("URFDLB"[i // 9] for i in range(54)) kociemba.solve("DRLUUBFBRBLURRLRUBLRDDFDLFUFUFFDBRDUBRUFLLFDDBFLUBLRBD") t = time.time() for _ in range(20): kociemba.solve("DRLUUBFBRBLURRLRUBLRDDFDLFUFUFFDBRDUBRUFLLFDDBFLUBLRBD") ms = (time.time() - t) / 20 * 1000 print(f"kociemba: {ms:.1f} ms/solve (native)") if ms > 200: sys.exit(f"ABORT: {ms:.0f} ms/solve is fallback-speed; refusing to generate") PYCHECK cd "${CODE_DIR:-/workspace/code}" mkdir -p data checkpoints # Smoke-test the GPU *before* generating anything. A torch build without kernels # for this GPU fails only when the first tensor op runs, which used to be training # -- so an unusable GPU was discovered an hour and a full dataset later. This # runs the real model for one forward and backward pass, so it catches missing # kernels rather than merely that a device is visible. echo "=== verifying GPU can run the model ===" /opt/venv/bin/python - <> data/train.jsonl echo "=== training set is now $(wc -l < data/train.jsonl) pairs ===" fi echo "=== verifying (must report 0 FAILED) ===" /opt/venv/bin/python verify_data.py data/train.jsonl --limit 100000 /opt/venv/bin/python verify_data.py data/val.jsonl # TRAIN_VALUE switches the objective from "emit a solution" to "estimate distance # to solved". The policy shape has been shown four times over not to reach past # ~8 moves; this is the heuristic that lets search steer. Same data either way -- # the label is just the solution length instead of the solution. if [ -n "${TRAIN_VALUE:-}" ]; then echo "=== training value function ===" /opt/venv/bin/python train_value.py \ --data data/train.jsonl --val-data data/val.jsonl \ --hidden "$HIDDEN" --layers "$LAYERS" --heads 8 \ --batch-size "${BATCH:-1024}" --lr 6e-4 --max-steps "$STEPS" \ --eval-every 1000 --eval-n 8192 \ --out checkpoints/value --hub-repo "${HUB_REPO:-}" echo "=== done ===" exit 0 fi echo "=== training ===" /opt/venv/bin/python train.py \ --data data/train.jsonl --val-data data/val.jsonl \ --hidden "$HIDDEN" --layers "$LAYERS" --heads 8 \ --batch-size "${BATCH:-1024}" --lr 6e-4 --max-steps "$STEPS" \ --eval-every 1000 --eval-n 512 \ --out checkpoints/cube --hub-repo "${HUB_REPO:-}" ${RESUME:+--resume "$RESUME"} echo "=== done ==="