sync training code: stage-1 instance-epoch sampler, multi-stage run, superposition metrics
Browse files- code/wavecurriculum_run/env_paths.sh +77 -0
- code/wavecurriculum_run/hf_push_login.py +191 -0
- code/wavecurriculum_run/hf_sync.py +253 -0
- code/wavecurriculum_run/sbatch_instance_latent.sh +191 -0
- code/wavecurriculum_run/sbatch_multistage_super.sh +188 -0
- code/wavecurriculum_run/sbatch_stage1_super.sh +187 -0
- code/wavecurriculum_run/sbatch_wave12.sh +203 -0
- code/wavecurriculum_run/show_val_example.py +63 -0
- code/wavecurriculum_run/test_instance_epochs.py +83 -0
- code/wavecurriculum_run/test_new_metrics.py +163 -0
- code/wavecurriculum_run/train/data.py +692 -0
- code/wavecurriculum_run/train/evaluater.py +723 -0
- code/wavecurriculum_run/train/main.py +336 -0
- code/wavecurriculum_run/train/model.py +232 -0
- code/wavecurriculum_run/train/train_and_evaluate.py +596 -0
- code/wavecurriculum_run/train/train_backtrack.py +763 -0
- code/wavecurriculum_run/train/trainer.py +383 -0
- code/wavecurriculum_run/verify_uniform_loader.py +132 -0
code/wavecurriculum_run/env_paths.sh
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Portable RUN_DIR / PY / CUDNN_LIB for slimgpu vs Berkeley (gandalf/feanor).
|
| 3 |
+
#
|
| 4 |
+
# Override any of these before sourcing:
|
| 5 |
+
# SUDOKU_RUN_DIR, SUDOKU_PY, SUDOKU_CUDNN_LIB
|
| 6 |
+
#
|
| 7 |
+
# Usage (from launch_*.sh):
|
| 8 |
+
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env_paths.sh"
|
| 9 |
+
|
| 10 |
+
_SLIM_ROOT="/egr/research-slim/ghoshavr/llm-reasoning-logic-puzzles"
|
| 11 |
+
_SLIM_RUN="${_SLIM_ROOT}/sudoku-code/multicandidate_run"
|
| 12 |
+
_SLIM_PY="/egr/research-slim/ghoshavr/conda-envs/logicpuzzles/bin/python"
|
| 13 |
+
_SLIM_CUDNN="/egr/research-slim/ghoshavr/conda-envs/logicpuzzles/lib/python3.9/site-packages/nvidia/cudnn/lib"
|
| 14 |
+
|
| 15 |
+
# Berkeley: prefer scratch (home often quota-limited), then $HOME (may be a symlink).
|
| 16 |
+
_BERK_RUN_SCRATCH="/scratch/users/gatmiry/llm-reasoning-logic-puzzles/sudoku-code/multicandidate_run"
|
| 17 |
+
_BERK_RUN="${HOME}/llm-reasoning-logic-puzzles/sudoku-code/multicandidate_run"
|
| 18 |
+
|
| 19 |
+
# Prefer explicit overrides, then slimgpu tree if present, else Berkeley home.
|
| 20 |
+
if [ -n "${SUDOKU_RUN_DIR:-}" ]; then
|
| 21 |
+
RUN_DIR="${SUDOKU_RUN_DIR}"
|
| 22 |
+
elif [ -d "${_SLIM_RUN}" ]; then
|
| 23 |
+
RUN_DIR="${_SLIM_RUN}"
|
| 24 |
+
elif [ -d "${_BERK_RUN_SCRATCH}" ]; then
|
| 25 |
+
RUN_DIR="${_BERK_RUN_SCRATCH}"
|
| 26 |
+
else
|
| 27 |
+
RUN_DIR="${_BERK_RUN}"
|
| 28 |
+
fi
|
| 29 |
+
|
| 30 |
+
if [ -n "${SUDOKU_PY:-}" ]; then
|
| 31 |
+
PY="${SUDOKU_PY}"
|
| 32 |
+
elif [ -x "${_SLIM_PY}" ]; then
|
| 33 |
+
PY="${_SLIM_PY}"
|
| 34 |
+
else
|
| 35 |
+
# Common Berkeley conda layouts; first existing wins.
|
| 36 |
+
for _cand in \
|
| 37 |
+
"${HOME}/miniconda3/envs/logic_puzzles/bin/python" \
|
| 38 |
+
"${HOME}/miniconda3/envs/logicpuzzles/bin/python" \
|
| 39 |
+
"${HOME}/anaconda3/envs/logic_puzzles/bin/python" \
|
| 40 |
+
"${HOME}/anaconda3/envs/logicpuzzles/bin/python" \
|
| 41 |
+
"${HOME}/.conda/envs/logic_puzzles/bin/python" \
|
| 42 |
+
"${HOME}/.conda/envs/logicpuzzles/bin/python" \
|
| 43 |
+
"/scratch/users/gatmiry/conda/envs/logic_puzzles/bin/python" \
|
| 44 |
+
"/scratch/users/gatmiry/conda/envs/logicpuzzles/bin/python"; do
|
| 45 |
+
if [ -x "${_cand}" ]; then
|
| 46 |
+
PY="${_cand}"
|
| 47 |
+
break
|
| 48 |
+
fi
|
| 49 |
+
done
|
| 50 |
+
if [ -z "${PY:-}" ]; then
|
| 51 |
+
if command -v python >/dev/null 2>&1; then
|
| 52 |
+
PY="$(command -v python)"
|
| 53 |
+
else
|
| 54 |
+
echo "ERROR: no Python found. Create env: conda env create -f ~/llm-reasoning-logic-puzzles/environment.yml -n logic_puzzles" >&2
|
| 55 |
+
echo " then: export SUDOKU_PY=\$HOME/miniconda3/envs/logic_puzzles/bin/python" >&2
|
| 56 |
+
return 1 2>/dev/null || exit 1
|
| 57 |
+
fi
|
| 58 |
+
fi
|
| 59 |
+
fi
|
| 60 |
+
|
| 61 |
+
if [ -n "${SUDOKU_CUDNN_LIB:-}" ]; then
|
| 62 |
+
CUDNN_LIB="${SUDOKU_CUDNN_LIB}"
|
| 63 |
+
elif [ -d "${_SLIM_CUDNN}" ]; then
|
| 64 |
+
CUDNN_LIB="${_SLIM_CUDNN}"
|
| 65 |
+
else
|
| 66 |
+
# Derive from PY site-packages if present; else leave empty (cluster CUDA often enough).
|
| 67 |
+
_py_root="$(cd "$(dirname "${PY}")/.." && pwd)"
|
| 68 |
+
_cudnn_guess="${_py_root}/lib/python3.9/site-packages/nvidia/cudnn/lib"
|
| 69 |
+
if [ -d "${_cudnn_guess}" ]; then
|
| 70 |
+
CUDNN_LIB="${_cudnn_guess}"
|
| 71 |
+
else
|
| 72 |
+
CUDNN_LIB=""
|
| 73 |
+
fi
|
| 74 |
+
fi
|
| 75 |
+
|
| 76 |
+
export RUN_DIR PY CUDNN_LIB
|
| 77 |
+
unset _SLIM_ROOT _SLIM_RUN _SLIM_PY _SLIM_CUDNN _BERK_RUN _cand _py_root _cudnn_guess
|
code/wavecurriculum_run/hf_push_login.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Upload scratch/gandalf staging files to Hugging Face.
|
| 3 |
+
|
| 4 |
+
Runs on the login node (python3 + huggingface_hub). The feanor sidecar only
|
| 5 |
+
needs to land files in --stage-dir; this process does the HTTP/LFS upload.
|
| 6 |
+
"""
|
| 7 |
+
import argparse
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import time
|
| 12 |
+
import traceback
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _load_json(path):
|
| 16 |
+
try:
|
| 17 |
+
with open(path) as f:
|
| 18 |
+
return json.load(f)
|
| 19 |
+
except Exception:
|
| 20 |
+
return {}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _write_json(path, payload):
|
| 24 |
+
tmp = path + ".tmp"
|
| 25 |
+
with open(tmp, "w") as f:
|
| 26 |
+
json.dump(payload, f, indent=2, sort_keys=True)
|
| 27 |
+
f.write("\n")
|
| 28 |
+
os.replace(tmp, path)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
_KEEP_HB = (
|
| 32 |
+
"event", "step", "stage", "loss",
|
| 33 |
+
"val_acc", "val_mass", "val_excess", "val_spread",
|
| 34 |
+
"loc_wave", "loc_coverage", "loc_lcs", "loc_dup",
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _slim_heartbeat(src, dest):
|
| 39 |
+
raw = _load_json(src)
|
| 40 |
+
slim = {k: raw[k] for k in _KEEP_HB if k in raw}
|
| 41 |
+
_write_json(dest, slim)
|
| 42 |
+
return dest
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _slim_log(src, dest):
|
| 46 |
+
"""Keep only eval / promote lines: step, stage, loss, accuracies."""
|
| 47 |
+
keep = []
|
| 48 |
+
try:
|
| 49 |
+
with open(src, errors="replace") as f:
|
| 50 |
+
for line in f:
|
| 51 |
+
s = line.strip()
|
| 52 |
+
if not s:
|
| 53 |
+
continue
|
| 54 |
+
if s.startswith("[curriculum]"):
|
| 55 |
+
keep.append(s + "\n")
|
| 56 |
+
continue
|
| 57 |
+
# "2000 stage 1 loss 1.47 loc_acc 0.03 val_acc 0.11 inset 0.66"
|
| 58 |
+
if " loss " in s and "stage" in s and not s.startswith("I"):
|
| 59 |
+
keep.append(s + "\n")
|
| 60 |
+
except OSError:
|
| 61 |
+
return None
|
| 62 |
+
with open(dest, "w") as f:
|
| 63 |
+
f.writelines(keep)
|
| 64 |
+
return dest
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _upload(api, token, local, repo, dest):
|
| 68 |
+
api.upload_file(
|
| 69 |
+
path_or_fileobj=local,
|
| 70 |
+
path_in_repo=dest,
|
| 71 |
+
repo_id=repo,
|
| 72 |
+
repo_type="model",
|
| 73 |
+
token=token,
|
| 74 |
+
commit_message=f"sync {dest}",
|
| 75 |
+
)
|
| 76 |
+
print(f"[hf_push] uploaded {dest} ({os.path.getsize(local)} bytes)", flush=True)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def sync_once(args, state, api, token):
|
| 80 |
+
prefix = args.prefix.strip("/")
|
| 81 |
+
repo = args.repo
|
| 82 |
+
|
| 83 |
+
for local, dest, key in (
|
| 84 |
+
(args.heartbeat, f"{prefix}/heartbeat.json", "heartbeat"),
|
| 85 |
+
(args.log, f"{prefix}/train.log", "log"),
|
| 86 |
+
):
|
| 87 |
+
if not local or not os.path.isfile(local):
|
| 88 |
+
continue
|
| 89 |
+
mtime = os.path.getmtime(local)
|
| 90 |
+
size = os.path.getsize(local)
|
| 91 |
+
last = state.get(key, {})
|
| 92 |
+
min_age = 0 if key == "heartbeat" else 300
|
| 93 |
+
due = (time.time() - float(last.get("uploaded_at", 0))) >= min_age
|
| 94 |
+
if due and (mtime > float(last.get("mtime", 0)) or size != last.get("size", -1)):
|
| 95 |
+
upload_path = local
|
| 96 |
+
if key == "heartbeat":
|
| 97 |
+
upload_path = _slim_heartbeat(
|
| 98 |
+
local, os.path.join(os.path.dirname(local) or ".",
|
| 99 |
+
"_hf_heartbeat_slim.json"))
|
| 100 |
+
elif key == "log":
|
| 101 |
+
upload_path = _slim_log(
|
| 102 |
+
local, os.path.join(os.path.dirname(local) or ".",
|
| 103 |
+
"_hf_train_slim.log"))
|
| 104 |
+
if upload_path:
|
| 105 |
+
_upload(api, token, upload_path, repo, dest)
|
| 106 |
+
state[key] = {"mtime": mtime, "size": size, "uploaded_at": time.time()}
|
| 107 |
+
|
| 108 |
+
if args.stage_dir and os.path.isdir(args.stage_dir):
|
| 109 |
+
for name in sorted(os.listdir(args.stage_dir)):
|
| 110 |
+
if name in ("hf_push_state.json", "heartbeat.json"):
|
| 111 |
+
continue
|
| 112 |
+
if not name.endswith(".tar") and name not in ("latest.json",):
|
| 113 |
+
continue
|
| 114 |
+
local = os.path.join(args.stage_dir, name)
|
| 115 |
+
if not os.path.isfile(local):
|
| 116 |
+
continue
|
| 117 |
+
# step_/stage_ names are unique per checkpoint, so once uploaded
|
| 118 |
+
# they never need revisiting. latest.tar/latest.json keep the same
|
| 119 |
+
# name and are rewritten every save, so skipping them by name would
|
| 120 |
+
# freeze the rolling checkpoint at whichever step happened to land
|
| 121 |
+
# first; track those by mtime instead.
|
| 122 |
+
rolling = name in ("latest.tar", "latest.json")
|
| 123 |
+
mtime = os.path.getmtime(local)
|
| 124 |
+
if rolling:
|
| 125 |
+
if mtime <= float(state.get("rolling", {}).get(name, 0)):
|
| 126 |
+
continue
|
| 127 |
+
elif name in state.get("staged", []):
|
| 128 |
+
continue
|
| 129 |
+
dest = f"{prefix}/{name}"
|
| 130 |
+
if name.startswith("stage"):
|
| 131 |
+
dest = f"{prefix}/stages/{name}"
|
| 132 |
+
elif name.startswith("step_"):
|
| 133 |
+
dest = f"{prefix}/steps/{name}"
|
| 134 |
+
_upload(api, token, local, repo, dest)
|
| 135 |
+
if rolling:
|
| 136 |
+
state.setdefault("rolling", {})[name] = mtime
|
| 137 |
+
continue # keep the file; the sidecar overwrites it in place
|
| 138 |
+
state.setdefault("staged", []).append(name)
|
| 139 |
+
# Drop the local copy after a successful upload to save quota.
|
| 140 |
+
try:
|
| 141 |
+
os.remove(local)
|
| 142 |
+
except OSError:
|
| 143 |
+
pass
|
| 144 |
+
return state
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def main():
|
| 148 |
+
ap = argparse.ArgumentParser()
|
| 149 |
+
ap.add_argument("--repo", default="Avra98/Sudoku_superposition")
|
| 150 |
+
ap.add_argument("--prefix", default="runs/w12_inst_latent")
|
| 151 |
+
ap.add_argument("--heartbeat", default="")
|
| 152 |
+
ap.add_argument("--log", default="")
|
| 153 |
+
ap.add_argument("--stage-dir", default="")
|
| 154 |
+
ap.add_argument("--token-file", default="/scratch/users/gatmiry/.hf_token")
|
| 155 |
+
ap.add_argument("--state", default="")
|
| 156 |
+
ap.add_argument("--interval", type=int, default=60)
|
| 157 |
+
ap.add_argument("--once", action="store_true")
|
| 158 |
+
args = ap.parse_args()
|
| 159 |
+
|
| 160 |
+
token = ""
|
| 161 |
+
if args.token_file and os.path.isfile(args.token_file):
|
| 162 |
+
with open(args.token_file) as f:
|
| 163 |
+
token = f.read().strip()
|
| 164 |
+
token = token or os.environ.get("HF_TOKEN", "")
|
| 165 |
+
if not token:
|
| 166 |
+
print("[hf_push] no token", file=sys.stderr)
|
| 167 |
+
return 1
|
| 168 |
+
|
| 169 |
+
from huggingface_hub import HfApi
|
| 170 |
+
api = HfApi(token=token)
|
| 171 |
+
state_path = args.state or os.path.join(
|
| 172 |
+
args.stage_dir or "/tmp/sudoku_hf_uploads", "hf_push_state.json")
|
| 173 |
+
os.makedirs(os.path.dirname(state_path) or ".", exist_ok=True)
|
| 174 |
+
state = _load_json(state_path)
|
| 175 |
+
print(f"[hf_push] watching heartbeat={args.heartbeat} log={args.log} "
|
| 176 |
+
f"stage={args.stage_dir}", flush=True)
|
| 177 |
+
while True:
|
| 178 |
+
try:
|
| 179 |
+
state = sync_once(args, state, api, token)
|
| 180 |
+
_write_json(state_path, state)
|
| 181 |
+
except Exception:
|
| 182 |
+
traceback.print_exc()
|
| 183 |
+
print("[hf_push] cycle failed; will retry", flush=True)
|
| 184 |
+
if args.once:
|
| 185 |
+
break
|
| 186 |
+
time.sleep(max(5, args.interval))
|
| 187 |
+
return 0
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
if __name__ == "__main__":
|
| 191 |
+
sys.exit(main())
|
code/wavecurriculum_run/hf_sync.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Sidecar: copy logs to scratch and push heartbeat/checkpoints to Hugging Face.
|
| 3 |
+
|
| 4 |
+
Does not train. Safe to run in a loop next to the job. Large Orbax dirs are
|
| 5 |
+
tarred, then uploaded as a single file so `latest.tar` can be overwritten.
|
| 6 |
+
"""
|
| 7 |
+
import argparse
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import socket
|
| 11 |
+
import subprocess
|
| 12 |
+
import sys
|
| 13 |
+
import time
|
| 14 |
+
import traceback
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _load_json(path):
|
| 18 |
+
try:
|
| 19 |
+
with open(path) as f:
|
| 20 |
+
return json.load(f)
|
| 21 |
+
except Exception:
|
| 22 |
+
return {}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _write_json(path, payload):
|
| 26 |
+
tmp = path + ".tmp"
|
| 27 |
+
with open(tmp, "w") as f:
|
| 28 |
+
json.dump(payload, f, indent=2, sort_keys=True)
|
| 29 |
+
f.write("\n")
|
| 30 |
+
os.replace(tmp, path)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _latest_ckpt_dir(workdir):
|
| 34 |
+
best = None
|
| 35 |
+
best_step = -1
|
| 36 |
+
try:
|
| 37 |
+
names = os.listdir(workdir)
|
| 38 |
+
except FileNotFoundError:
|
| 39 |
+
return None, -1
|
| 40 |
+
for name in names:
|
| 41 |
+
if not name.startswith("checkpoint_"):
|
| 42 |
+
continue
|
| 43 |
+
if name.endswith(".orbax-checkpoint-tmp") or ".orbax-checkpoint-tmp" in name:
|
| 44 |
+
continue
|
| 45 |
+
path = os.path.join(workdir, name)
|
| 46 |
+
if not os.path.isdir(path):
|
| 47 |
+
continue
|
| 48 |
+
try:
|
| 49 |
+
step = int(name.split("_", 1)[1])
|
| 50 |
+
except ValueError:
|
| 51 |
+
continue
|
| 52 |
+
if step > best_step:
|
| 53 |
+
best_step = step
|
| 54 |
+
best = path
|
| 55 |
+
return best, best_step
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _stage_ckpt_dirs(stage_dir):
|
| 59 |
+
out = []
|
| 60 |
+
if not os.path.isdir(stage_dir):
|
| 61 |
+
return out
|
| 62 |
+
for name in sorted(os.listdir(stage_dir)):
|
| 63 |
+
path = os.path.join(stage_dir, name)
|
| 64 |
+
if os.path.isdir(path) and not name.endswith(".tmp"):
|
| 65 |
+
out.append(path)
|
| 66 |
+
return out
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _tar(src_dir, dest_tar):
|
| 70 |
+
parent = os.path.dirname(os.path.abspath(src_dir))
|
| 71 |
+
base = os.path.basename(src_dir.rstrip("/"))
|
| 72 |
+
tmp = dest_tar + ".tmp"
|
| 73 |
+
subprocess.check_call(
|
| 74 |
+
["tar", "-C", parent, "-cf", tmp, base],
|
| 75 |
+
stdout=subprocess.DEVNULL,
|
| 76 |
+
stderr=subprocess.DEVNULL,
|
| 77 |
+
)
|
| 78 |
+
os.replace(tmp, dest_tar)
|
| 79 |
+
return dest_tar
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _hf_api(token):
|
| 83 |
+
try:
|
| 84 |
+
from huggingface_hub import HfApi
|
| 85 |
+
except Exception as exc:
|
| 86 |
+
print(f"[hf_sync] huggingface_hub unavailable ({exc}); "
|
| 87 |
+
f"will stage files for the login-node pusher", flush=True)
|
| 88 |
+
return None
|
| 89 |
+
return HfApi(token=token)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _rsync_to_login(local, remote_dir):
|
| 93 |
+
"""Copy a file to gandalf so the login-node pusher can upload it."""
|
| 94 |
+
if not remote_dir or not os.path.isfile(local):
|
| 95 |
+
return False
|
| 96 |
+
dest = remote_dir.rstrip("/") + "/"
|
| 97 |
+
cmd = ["rsync", "-a", "-e",
|
| 98 |
+
"ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new",
|
| 99 |
+
local, dest]
|
| 100 |
+
try:
|
| 101 |
+
subprocess.check_call(cmd, stdout=subprocess.DEVNULL,
|
| 102 |
+
stderr=subprocess.DEVNULL)
|
| 103 |
+
print(f"[hf_sync] staged {os.path.basename(local)} -> {dest}", flush=True)
|
| 104 |
+
return True
|
| 105 |
+
except Exception as exc:
|
| 106 |
+
print(f"[hf_sync] rsync to login failed: {exc}", flush=True)
|
| 107 |
+
return False
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _upload_file(api, local, repo, dest, token):
|
| 111 |
+
if api is None:
|
| 112 |
+
print(f"[hf_sync] skip upload {dest}: huggingface_hub missing", flush=True)
|
| 113 |
+
return False
|
| 114 |
+
api.upload_file(
|
| 115 |
+
path_or_fileobj=local,
|
| 116 |
+
path_in_repo=dest,
|
| 117 |
+
repo_id=repo,
|
| 118 |
+
repo_type="model",
|
| 119 |
+
token=token,
|
| 120 |
+
commit_message=f"sync {dest}",
|
| 121 |
+
)
|
| 122 |
+
print(f"[hf_sync] uploaded {dest} ({os.path.getsize(local)} bytes)", flush=True)
|
| 123 |
+
return True
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def sync_once(args, state):
|
| 127 |
+
workdir = os.path.abspath(args.workdir)
|
| 128 |
+
scratch_log = args.scratch_log
|
| 129 |
+
if args.log and os.path.isfile(args.log) and scratch_log:
|
| 130 |
+
os.makedirs(os.path.dirname(scratch_log), exist_ok=True)
|
| 131 |
+
subprocess.call(["rsync", "-a", args.log, scratch_log])
|
| 132 |
+
|
| 133 |
+
hb_src = os.path.join(workdir, "heartbeat.json")
|
| 134 |
+
if os.path.isfile(hb_src) and scratch_log:
|
| 135 |
+
hb_scratch = os.path.join(os.path.dirname(scratch_log), "w12_inst_latent_heartbeat.json")
|
| 136 |
+
subprocess.call(["rsync", "-a", hb_src, hb_scratch])
|
| 137 |
+
|
| 138 |
+
token = (os.environ.get("HF_TOKEN")
|
| 139 |
+
or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
| 140 |
+
or "")
|
| 141 |
+
if args.token_file and os.path.isfile(args.token_file):
|
| 142 |
+
with open(args.token_file) as f:
|
| 143 |
+
token = f.read().strip() or token
|
| 144 |
+
if not token:
|
| 145 |
+
print("[hf_sync] no HF token; scratch copy only", flush=True)
|
| 146 |
+
return state
|
| 147 |
+
|
| 148 |
+
api = _hf_api(token)
|
| 149 |
+
prefix = args.prefix.strip("/")
|
| 150 |
+
repo = args.repo
|
| 151 |
+
|
| 152 |
+
if os.path.isfile(hb_src):
|
| 153 |
+
mtime = os.path.getmtime(hb_src)
|
| 154 |
+
if mtime > float(state.get("heartbeat_mtime", 0)):
|
| 155 |
+
if _upload_file(api, hb_src, repo, f"{prefix}/heartbeat.json", token):
|
| 156 |
+
state["heartbeat_mtime"] = mtime
|
| 157 |
+
_rsync_to_login(hb_src, args.login_stage)
|
| 158 |
+
|
| 159 |
+
if args.log and os.path.isfile(args.log):
|
| 160 |
+
mtime = os.path.getmtime(args.log)
|
| 161 |
+
size = os.path.getsize(args.log)
|
| 162 |
+
last = state.get("log", {})
|
| 163 |
+
due = (time.time() - float(last.get("uploaded_at", 0))) >= 300
|
| 164 |
+
if due and (mtime > float(last.get("mtime", 0)) or size != last.get("size", -1)):
|
| 165 |
+
if _upload_file(api, args.log, repo, f"{prefix}/train.log", token):
|
| 166 |
+
state["log"] = {"mtime": mtime, "size": size,
|
| 167 |
+
"uploaded_at": time.time()}
|
| 168 |
+
|
| 169 |
+
ready = _load_json(os.path.join(workdir, "ckpt_ready.json"))
|
| 170 |
+
ckpt_dir, ckpt_step = _latest_ckpt_dir(workdir)
|
| 171 |
+
if ckpt_dir and ckpt_step > int(state.get("latest_step", -1)):
|
| 172 |
+
tar_path = os.path.join(args.tar_dir, "latest.tar")
|
| 173 |
+
print(f"[hf_sync] tarring {ckpt_dir} -> {tar_path}", flush=True)
|
| 174 |
+
_tar(ckpt_dir, tar_path)
|
| 175 |
+
uploaded = _upload_file(api, tar_path, repo, f"{prefix}/latest.tar", token)
|
| 176 |
+
staged = _rsync_to_login(tar_path, args.login_stage)
|
| 177 |
+
if uploaded or staged:
|
| 178 |
+
state["latest_step"] = ckpt_step
|
| 179 |
+
meta = {
|
| 180 |
+
"step": ckpt_step,
|
| 181 |
+
"event": ready.get("event", "periodic"),
|
| 182 |
+
"stage": ready.get("stage"),
|
| 183 |
+
"host": socket.gethostname(),
|
| 184 |
+
"job_id": os.environ.get("SLURM_JOB_ID", ""),
|
| 185 |
+
"src": ckpt_dir,
|
| 186 |
+
}
|
| 187 |
+
meta_path = os.path.join(args.tar_dir, "latest.json")
|
| 188 |
+
_write_json(meta_path, meta)
|
| 189 |
+
_upload_file(api, meta_path, repo, f"{prefix}/latest.json", token)
|
| 190 |
+
_rsync_to_login(meta_path, args.login_stage)
|
| 191 |
+
if uploaded:
|
| 192 |
+
state["latest_step"] = ckpt_step
|
| 193 |
+
keep = bool(ready.get("keep_snapshot")) or (ckpt_step > 0 and ckpt_step % 50000 == 0)
|
| 194 |
+
if keep and ckpt_step not in set(state.get("snapshots", [])):
|
| 195 |
+
dest = f"{prefix}/steps/step_{ckpt_step}.tar"
|
| 196 |
+
snap = os.path.join(args.tar_dir, f"step_{ckpt_step}.tar")
|
| 197 |
+
if snap != tar_path:
|
| 198 |
+
subprocess.call(["/bin/cp", "-f", tar_path, snap])
|
| 199 |
+
if _upload_file(api, tar_path, repo, dest, token):
|
| 200 |
+
state.setdefault("snapshots", []).append(ckpt_step)
|
| 201 |
+
_rsync_to_login(snap if os.path.isfile(snap) else tar_path, args.login_stage)
|
| 202 |
+
|
| 203 |
+
stage_dir = os.path.join(workdir, "stage_ckpts")
|
| 204 |
+
uploaded_stages = set(state.get("stages", []))
|
| 205 |
+
for path in _stage_ckpt_dirs(stage_dir):
|
| 206 |
+
name = os.path.basename(path)
|
| 207 |
+
if name in uploaded_stages:
|
| 208 |
+
continue
|
| 209 |
+
tar_path = os.path.join(args.tar_dir, f"{name}.tar")
|
| 210 |
+
print(f"[hf_sync] tarring stage {path}", flush=True)
|
| 211 |
+
_tar(path, tar_path)
|
| 212 |
+
if _upload_file(api, tar_path, repo, f"{prefix}/stages/{name}.tar", token):
|
| 213 |
+
uploaded_stages.add(name)
|
| 214 |
+
_rsync_to_login(tar_path, args.login_stage)
|
| 215 |
+
state["stages"] = sorted(uploaded_stages)
|
| 216 |
+
return state
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def main():
|
| 220 |
+
ap = argparse.ArgumentParser()
|
| 221 |
+
ap.add_argument("--workdir", required=True)
|
| 222 |
+
ap.add_argument("--log", default="")
|
| 223 |
+
ap.add_argument("--scratch-log", default="")
|
| 224 |
+
ap.add_argument("--repo", default="Avra98/Sudoku_superposition")
|
| 225 |
+
ap.add_argument("--prefix", default="runs/w12_inst_latent")
|
| 226 |
+
ap.add_argument("--token-file", default="")
|
| 227 |
+
ap.add_argument("--state", default="")
|
| 228 |
+
ap.add_argument("--tar-dir", default="")
|
| 229 |
+
ap.add_argument("--interval", type=int, default=60)
|
| 230 |
+
ap.add_argument("--login-stage", default="gandalf.berkeley.edu:/tmp/sudoku_hf_uploads")
|
| 231 |
+
ap.add_argument("--once", action="store_true")
|
| 232 |
+
args = ap.parse_args()
|
| 233 |
+
args.tar_dir = args.tar_dir or os.path.join(os.path.abspath(args.workdir), "_hf_tars")
|
| 234 |
+
os.makedirs(args.tar_dir, exist_ok=True)
|
| 235 |
+
state_path = args.state or os.path.join(args.tar_dir, "hf_sync_state.json")
|
| 236 |
+
state = _load_json(state_path)
|
| 237 |
+
print(f"[hf_sync] host={socket.gethostname()} workdir={args.workdir}",
|
| 238 |
+
flush=True)
|
| 239 |
+
while True:
|
| 240 |
+
try:
|
| 241 |
+
state = sync_once(args, state)
|
| 242 |
+
_write_json(state_path, state)
|
| 243 |
+
except Exception:
|
| 244 |
+
traceback.print_exc()
|
| 245 |
+
print("[hf_sync] cycle failed; will retry", flush=True)
|
| 246 |
+
if args.once:
|
| 247 |
+
break
|
| 248 |
+
time.sleep(max(5, args.interval))
|
| 249 |
+
return 0
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
if __name__ == "__main__":
|
| 253 |
+
sys.exit(main())
|
code/wavecurriculum_run/sbatch_instance_latent.sh
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#SBATCH --partition=songmei
|
| 3 |
+
#SBATCH --nodelist=feanor
|
| 4 |
+
#SBATCH --gres=gpu:1
|
| 5 |
+
#SBATCH --cpus-per-task=8
|
| 6 |
+
#SBATCH --mem=64G
|
| 7 |
+
#SBATCH --time=72:00:00
|
| 8 |
+
#SBATCH --job-name=w12_inst
|
| 9 |
+
#SBATCH --output=/scratch/users/gatmiry/llm-reasoning-logic-puzzles/sudoku-code/wavecurriculum_run/logs/slurm_%x_%j.out
|
| 10 |
+
#SBATCH --error=/scratch/users/gatmiry/llm-reasoning-logic-puzzles/sudoku-code/wavecurriculum_run/logs/slurm_%x_%j.err
|
| 11 |
+
|
| 12 |
+
# Same recipe as the slimgpu w12_inst_latent run:
|
| 13 |
+
# K=12 recurrent latent curriculum, no backtrack, no candidate-head BCE.
|
| 14 |
+
# Output prompt = one sampled stage-k instance (CE against that sequence).
|
| 15 |
+
# Instance npy files and s12 masks live on feanor /tmp (scratch quota is 20 G).
|
| 16 |
+
|
| 17 |
+
set -u
|
| 18 |
+
hostname
|
| 19 |
+
nvidia-smi -L
|
| 20 |
+
echo "[$(date)] w12_inst_latent"
|
| 21 |
+
|
| 22 |
+
SCRATCH_ROOT=/scratch/users/gatmiry/llm-reasoning-logic-puzzles
|
| 23 |
+
RUN_DIR=${SCRATCH_ROOT}/sudoku-code/wavecurriculum_run
|
| 24 |
+
ENV_LOCAL=/tmp/logicpuzzles
|
| 25 |
+
CAND_DIR=/tmp/sudoku_s12
|
| 26 |
+
INST_DIR=/tmp/sudoku_superposition
|
| 27 |
+
LOCAL_LOG=/tmp/sudoku_wave_runs/w12_inst_latent
|
| 28 |
+
TARBALL_GANDALF=/tmp/logicpuzzles_env.tar.gz
|
| 29 |
+
TARBALL_LOCAL=/tmp/logicpuzzles_env_${SLURM_JOB_ID}.tar.gz
|
| 30 |
+
GANDALF_INST=gandalf.berkeley.edu:/tmp/sudoku_superposition
|
| 31 |
+
GANDALF_CAND=gandalf.berkeley.edu:/tmp/sudoku_s12
|
| 32 |
+
|
| 33 |
+
mkdir -p "${RUN_DIR}/logs" "${LOCAL_LOG}" /tmp/sudoku_wave_runs
|
| 34 |
+
echo "[$(date)] /tmp before cleanup:"
|
| 35 |
+
df -h /tmp | tail -1
|
| 36 |
+
# Drop leftover run dirs from dead jobs; keep env + instance/mask caches.
|
| 37 |
+
rm -rf /tmp/sudoku_wave_runs
|
| 38 |
+
mkdir -p "${LOCAL_LOG}"
|
| 39 |
+
echo "[$(date)] /tmp after cleanup:"
|
| 40 |
+
df -h /tmp | tail -1
|
| 41 |
+
|
| 42 |
+
SCP_OPTS="-o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
|
| 43 |
+
[ -f "${HOME}/.ssh/id_ed25519_berkeley" ] && SCP_OPTS="${SCP_OPTS} -i ${HOME}/.ssh/id_ed25519_berkeley"
|
| 44 |
+
|
| 45 |
+
if ${ENV_LOCAL}/bin/python -u -c "import jax; assert jax.default_backend()=='gpu' or 'cuda' in str(jax.devices()[0]).lower()" 2>/dev/null; then
|
| 46 |
+
echo "[$(date)] reusing ${ENV_LOCAL}"
|
| 47 |
+
else
|
| 48 |
+
echo "[$(date)] fetching env tarball"
|
| 49 |
+
rm -rf "${ENV_LOCAL}"
|
| 50 |
+
scp ${SCP_OPTS} "gandalf.berkeley.edu:${TARBALL_GANDALF}" "${TARBALL_LOCAL}"
|
| 51 |
+
tar xzf "${TARBALL_LOCAL}" -C /tmp
|
| 52 |
+
rm -f "${TARBALL_LOCAL}"
|
| 53 |
+
fi
|
| 54 |
+
|
| 55 |
+
export PY=${ENV_LOCAL}/bin/python
|
| 56 |
+
export LD_LIBRARY_PATH=\
|
| 57 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cudnn/lib:\
|
| 58 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cublas/lib:\
|
| 59 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_runtime/lib:\
|
| 60 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_nvrtc/lib:\
|
| 61 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/nccl/lib:\
|
| 62 |
+
${LD_LIBRARY_PATH:-}
|
| 63 |
+
|
| 64 |
+
${PY} -u -c "import jax; print(jax.__version__, jax.devices(), jax.default_backend())"
|
| 65 |
+
|
| 66 |
+
# ---- Stage instance files onto node-local /tmp (scratch cannot hold 8 G) ----
|
| 67 |
+
need_inst=0
|
| 68 |
+
for f in train_assignments.npy train_starts.npy train_counts.npy \
|
| 69 |
+
test_assignments.npy test_starts.npy test_counts.npy; do
|
| 70 |
+
[ -s "${INST_DIR}/${f}" ] || need_inst=1
|
| 71 |
+
done
|
| 72 |
+
if [ "${need_inst}" = 1 ]; then
|
| 73 |
+
echo "[$(date)] pulling instances from ${GANDALF_INST}"
|
| 74 |
+
mkdir -p "${INST_DIR}"
|
| 75 |
+
rsync -a --progress -e "ssh ${SCP_OPTS}" \
|
| 76 |
+
"${GANDALF_INST}/" "${INST_DIR}/"
|
| 77 |
+
fi
|
| 78 |
+
for f in train_assignments.npy train_starts.npy train_counts.npy \
|
| 79 |
+
test_assignments.npy test_starts.npy test_counts.npy; do
|
| 80 |
+
[ -s "${INST_DIR}/${f}" ] || { echo "missing ${INST_DIR}/${f}" >&2; exit 1; }
|
| 81 |
+
done
|
| 82 |
+
|
| 83 |
+
need_cand=0
|
| 84 |
+
for f in train_cand_masks.npy test_cand_masks.npy; do
|
| 85 |
+
[ -s "${CAND_DIR}/${f}" ] || need_cand=1
|
| 86 |
+
done
|
| 87 |
+
if [ "${need_cand}" = 1 ]; then
|
| 88 |
+
echo "[$(date)] pulling s12 masks from ${GANDALF_CAND}"
|
| 89 |
+
mkdir -p "${CAND_DIR}"
|
| 90 |
+
rsync -a -e "ssh ${SCP_OPTS}" "${GANDALF_CAND}/" "${CAND_DIR}/" || true
|
| 91 |
+
fi
|
| 92 |
+
for f in train_cand_masks.npy test_cand_masks.npy; do
|
| 93 |
+
[ -s "${CAND_DIR}/${f}" ] || { echo "missing ${CAND_DIR}/${f}" >&2; exit 1; }
|
| 94 |
+
done
|
| 95 |
+
|
| 96 |
+
# ---- Exact same recipe as slimgpu w12_inst_latent ----
|
| 97 |
+
export SUDOKU_RESUME=0
|
| 98 |
+
export SUDOKU_START_STAGE=1
|
| 99 |
+
export SUDOKU_MAX_STAGE=12
|
| 100 |
+
export SUDOKU_LATENT_SLOTS=12
|
| 101 |
+
export SUDOKU_RECURRENT=1
|
| 102 |
+
export SUDOKU_BACKTRACK=0
|
| 103 |
+
export SUDOKU_CAND_SLOT_MODE=depth
|
| 104 |
+
export SUDOKU_PASSES_PER_STAGE=1
|
| 105 |
+
export SUDOKU_AUX_WEIGHT=0.0
|
| 106 |
+
export SUDOKU_LEVEL_BALANCED=0
|
| 107 |
+
export SUDOKU_DATA_CURRICULUM=none
|
| 108 |
+
export SUDOKU_PLATEAU_STEPS=20000
|
| 109 |
+
export SUDOKU_PLATEAU_DELTA=0.005
|
| 110 |
+
# 12 stages x 40k is 480k of the 800k budget, so the hard cap can no longer
|
| 111 |
+
# consume the whole run the way 80k did.
|
| 112 |
+
export SUDOKU_PATIENCE=40000
|
| 113 |
+
export SUDOKU_MIN_STAGE_STEPS=8000
|
| 114 |
+
# Stage t -> t+1 requires val_mass >= 0.85: the probability mass the value head
|
| 115 |
+
# puts on stage t's candidate set, measured teacher-forced. Chance is ~0.41 on
|
| 116 |
+
# stage-0 sets. This replaces the old in-set rate, which was conditioned on an
|
| 117 |
+
# exact positional location match and so was estimated from ~4% of steps.
|
| 118 |
+
export SUDOKU_PROMOTE_ACC=0.85
|
| 119 |
+
# Both extra gates start OFF: this run is the one that tells us the reachable
|
| 120 |
+
# range of loc_wave and val_excess. Turn them on once we can read the scale.
|
| 121 |
+
export SUDOKU_PROMOTE_LOC_WAVE=0.0
|
| 122 |
+
export SUDOKU_PROMOTE_VAL_EXCESS=0.0
|
| 123 |
+
export SUDOKU_MAX_STEPS="${SUDOKU_MAX_STEPS:-800000}"
|
| 124 |
+
export SUDOKU_EVAL_EVERY=2000
|
| 125 |
+
export SUDOKU_SAVE_EVERY=10000
|
| 126 |
+
export SUDOKU_CKPT_KEEP=2
|
| 127 |
+
export SUDOKU_LR=0.0002
|
| 128 |
+
export SUDOKU_DROPOUT=0.2
|
| 129 |
+
export SUDOKU_WD=0.005
|
| 130 |
+
export SUDOKU_TRAIN_PATH="${SCRATCH_ROOT}/sudoku-code/datasets/train_sudoku_puzzles.npy"
|
| 131 |
+
export SUDOKU_TEST_PATH="${SCRATCH_ROOT}/sudoku-code/datasets/test_sudoku_puzzles.npy"
|
| 132 |
+
export SUDOKU_TRAIN_CAND="${CAND_DIR}/train_cand_masks.npy"
|
| 133 |
+
export SUDOKU_TEST_CAND="${CAND_DIR}/test_cand_masks.npy"
|
| 134 |
+
export SUDOKU_INSTANCE_DIR="${INST_DIR}"
|
| 135 |
+
# Leave a little H200 headroom so eval + sidecar tar do not OOM the node.
|
| 136 |
+
export XLA_PYTHON_CLIENT_MEM_FRACTION=0.85
|
| 137 |
+
|
| 138 |
+
HF_TOKEN_FILE="${HF_TOKEN_FILE:-/scratch/users/gatmiry/.hf_token}"
|
| 139 |
+
if [ -z "${HF_TOKEN:-}" ] && [ -s "${HF_TOKEN_FILE}" ]; then
|
| 140 |
+
HF_TOKEN="$(cat "${HF_TOKEN_FILE}")"
|
| 141 |
+
export HF_TOKEN
|
| 142 |
+
fi
|
| 143 |
+
export HUGGING_FACE_HUB_TOKEN="${HF_TOKEN:-}"
|
| 144 |
+
HF_PKGS="${HF_PKGS:-/scratch/users/gatmiry/hf_pkgs}"
|
| 145 |
+
TRAIN_LOG="${RUN_DIR}/logs/w12_inst_latent.log"
|
| 146 |
+
SYNC_LOG="${RUN_DIR}/logs/hf_sync_w12_inst_latent.log"
|
| 147 |
+
|
| 148 |
+
# Need room for 2 rolling ckpts (~1 GB) plus a tar in flight.
|
| 149 |
+
tmp_avail_kb=$(df -Pk /tmp | awk 'NR==2{print $4}')
|
| 150 |
+
echo "[$(date)] /tmp avail ${tmp_avail_kb} KB"
|
| 151 |
+
if [ "${tmp_avail_kb}" -lt 3000000 ]; then
|
| 152 |
+
echo "ERROR: /tmp has less than 3G free; refusing to start" >&2
|
| 153 |
+
df -h /tmp
|
| 154 |
+
du -sh /tmp/* 2>/dev/null | sort -h | tail -20 >&2 || true
|
| 155 |
+
exit 1
|
| 156 |
+
fi
|
| 157 |
+
|
| 158 |
+
cd "${RUN_DIR}"
|
| 159 |
+
# huggingface_hub lives on scratch (feanor /tmp is too full to pip-install).
|
| 160 |
+
PYTHONPATH="${HF_PKGS}${PYTHONPATH:+:${PYTHONPATH}}" ${PY} -u "${RUN_DIR}/hf_sync.py" \
|
| 161 |
+
--workdir "${LOCAL_LOG}" \
|
| 162 |
+
--log "${TRAIN_LOG}" \
|
| 163 |
+
--scratch-log "${TRAIN_LOG}" \
|
| 164 |
+
--repo Avra98/Sudoku_superposition \
|
| 165 |
+
--prefix runs/w12_inst_latent \
|
| 166 |
+
--token-file "${HF_TOKEN_FILE}" \
|
| 167 |
+
--interval 60 \
|
| 168 |
+
> "${SYNC_LOG}" 2>&1 &
|
| 169 |
+
SYNC_PID=$!
|
| 170 |
+
|
| 171 |
+
echo "[$(date)] starting w12_inst_latent from scratch"
|
| 172 |
+
echo " K=12 recurrent=1 bt=0 aux=0 instance_dir=${INST_DIR}"
|
| 173 |
+
echo " workdir=${LOCAL_LOG} log=${TRAIN_LOG}"
|
| 174 |
+
echo " hf=Avra98/Sudoku_superposition/runs/w12_inst_latent"
|
| 175 |
+
# Log goes to scratch immediately so a /tmp-full crash still leaves a traceback.
|
| 176 |
+
CUDA_VISIBLE_DEVICES=0 ${PY} -u -m train.main \
|
| 177 |
+
--workdir="${LOCAL_LOG}" --exp_name="w12_inst_latent" \
|
| 178 |
+
> "${TRAIN_LOG}" 2>&1
|
| 179 |
+
EC=$?
|
| 180 |
+
kill ${SYNC_PID} 2>/dev/null || true
|
| 181 |
+
wait ${SYNC_PID} 2>/dev/null || true
|
| 182 |
+
PYTHONPATH="${HF_PKGS}${PYTHONPATH:+:${PYTHONPATH}}" ${PY} -u "${RUN_DIR}/hf_sync.py" --once \
|
| 183 |
+
--workdir "${LOCAL_LOG}" \
|
| 184 |
+
--log "${TRAIN_LOG}" \
|
| 185 |
+
--scratch-log "${TRAIN_LOG}" \
|
| 186 |
+
--repo Avra98/Sudoku_superposition \
|
| 187 |
+
--prefix runs/w12_inst_latent \
|
| 188 |
+
--token-file "${HF_TOKEN_FILE}" \
|
| 189 |
+
>> "${SYNC_LOG}" 2>&1 || true
|
| 190 |
+
echo "[$(date)] w12_inst_latent exit ${EC}"
|
| 191 |
+
exit ${EC}
|
code/wavecurriculum_run/sbatch_multistage_super.sh
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#SBATCH --partition=songmei
|
| 3 |
+
#SBATCH --nodelist=feanor
|
| 4 |
+
#SBATCH --gres=gpu:1
|
| 5 |
+
#SBATCH --cpus-per-task=8
|
| 6 |
+
#SBATCH --mem=64G
|
| 7 |
+
#SBATCH --time=72:00:00
|
| 8 |
+
#SBATCH --job-name=w12_multi
|
| 9 |
+
#SBATCH --output=/scratch/users/gatmiry/llm-reasoning-logic-puzzles/sudoku-code/wavecurriculum_run/logs/slurm_%x_%j.out
|
| 10 |
+
#SBATCH --error=/scratch/users/gatmiry/llm-reasoning-logic-puzzles/sudoku-code/wavecurriculum_run/logs/slurm_%x_%j.err
|
| 11 |
+
|
| 12 |
+
# Multi-stage superposition run, warm-started from the stage-1 checkpoint of
|
| 13 |
+
# w12_s1_super and walking stages 2..12. Runs on a SECOND GPU; the stage-1 job
|
| 14 |
+
# keeps going untouched.
|
| 15 |
+
#
|
| 16 |
+
# Every stage repeats the stage-1 recipe: that stage's own (puzzle, instance)
|
| 17 |
+
# pairs are enumerated and walked in shuffled passes, so each instance of each
|
| 18 |
+
# puzzle is seen at least 5 times before the stage can be left. The pool is
|
| 19 |
+
# rebuilt on promotion because each stage has its own instances.
|
| 20 |
+
#
|
| 21 |
+
# Promotion needs BOTH superposition gates to clear, no plateau and no patience:
|
| 22 |
+
# mass* >= SUDOKU_PROMOTE_ACC mass sits inside the candidate set
|
| 23 |
+
# spread* >= SUDOKU_PROMOTE_SPREAD and is spread evenly over it
|
| 24 |
+
# plus SUDOKU_MIN_STAGE_STEPS, which is set to cover 5 passes at every stage
|
| 25 |
+
# (stage 2 is the largest at 46,555 steps for 100k puzzles).
|
| 26 |
+
#
|
| 27 |
+
# IMPORTANT: this script must never wipe /tmp/sudoku_wave_runs, because the
|
| 28 |
+
# stage-1 job's live checkpoints are in there.
|
| 29 |
+
|
| 30 |
+
set -u
|
| 31 |
+
hostname
|
| 32 |
+
nvidia-smi -L
|
| 33 |
+
echo "[$(date)] w12_multistage"
|
| 34 |
+
|
| 35 |
+
SCRATCH_ROOT=/scratch/users/gatmiry/llm-reasoning-logic-puzzles
|
| 36 |
+
RUN_DIR=${SCRATCH_ROOT}/sudoku-code/wavecurriculum_run
|
| 37 |
+
ENV_LOCAL=/tmp/logicpuzzles
|
| 38 |
+
CAND_DIR=/tmp/sudoku_s12
|
| 39 |
+
INST_DIR=/tmp/sudoku_superposition
|
| 40 |
+
LOCAL_LOG=/tmp/sudoku_wave_runs/w12_multistage
|
| 41 |
+
WARM_SRC=/tmp/sudoku_wave_runs/w12_s1_super
|
| 42 |
+
WARM_DIR=/tmp/sudoku_warmstart_s1
|
| 43 |
+
LOGIN_STAGE=gandalf.berkeley.edu:/tmp/sudoku_hf_multistage
|
| 44 |
+
|
| 45 |
+
mkdir -p "${RUN_DIR}/logs" "${LOCAL_LOG}" "${WARM_DIR}"
|
| 46 |
+
echo "[$(date)] /tmp free:"
|
| 47 |
+
df -h /tmp | tail -1
|
| 48 |
+
|
| 49 |
+
SCP_OPTS="-o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
|
| 50 |
+
[ -f "${HOME}/.ssh/id_ed25519_berkeley" ] && SCP_OPTS="${SCP_OPTS} -i ${HOME}/.ssh/id_ed25519_berkeley"
|
| 51 |
+
|
| 52 |
+
if ${ENV_LOCAL}/bin/python -u -c "import jax; assert jax.default_backend()=='gpu' or 'cuda' in str(jax.devices()[0]).lower()" 2>/dev/null; then
|
| 53 |
+
echo "[$(date)] reusing ${ENV_LOCAL}"
|
| 54 |
+
else
|
| 55 |
+
echo "ERROR: ${ENV_LOCAL} missing; the stage-1 job should have created it" >&2
|
| 56 |
+
exit 1
|
| 57 |
+
fi
|
| 58 |
+
|
| 59 |
+
export PY=${ENV_LOCAL}/bin/python
|
| 60 |
+
export LD_LIBRARY_PATH=\
|
| 61 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cudnn/lib:\
|
| 62 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cublas/lib:\
|
| 63 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_runtime/lib:\
|
| 64 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_nvrtc/lib:\
|
| 65 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/nccl/lib:\
|
| 66 |
+
${LD_LIBRARY_PATH:-}
|
| 67 |
+
|
| 68 |
+
${PY} -u -c "import jax; print(jax.__version__, jax.devices(), jax.default_backend())"
|
| 69 |
+
|
| 70 |
+
for f in train_assignments.npy train_starts.npy train_counts.npy \
|
| 71 |
+
test_assignments.npy test_starts.npy test_counts.npy; do
|
| 72 |
+
[ -s "${INST_DIR}/${f}" ] || { echo "missing ${INST_DIR}/${f}" >&2; exit 1; }
|
| 73 |
+
done
|
| 74 |
+
for f in train_cand_masks.npy test_cand_masks.npy; do
|
| 75 |
+
[ -s "${CAND_DIR}/${f}" ] || { echo "missing ${CAND_DIR}/${f}" >&2; exit 1; }
|
| 76 |
+
done
|
| 77 |
+
|
| 78 |
+
# ---- Warm start: snapshot the stage-1 job's newest checkpoint ----
|
| 79 |
+
# Copied rather than read in place, so a concurrent write by the stage-1 job
|
| 80 |
+
# cannot be observed half-finished, and so this run never writes into its dir.
|
| 81 |
+
NEWEST=$(ls -1d ${WARM_SRC}/checkpoint_* 2>/dev/null \
|
| 82 |
+
| grep -v 'orbax-checkpoint-tmp' \
|
| 83 |
+
| sed 's/.*checkpoint_//' | sort -n | tail -1)
|
| 84 |
+
if [ -z "${NEWEST}" ]; then
|
| 85 |
+
echo "ERROR: no checkpoint_* found in ${WARM_SRC}" >&2
|
| 86 |
+
ls -la "${WARM_SRC}" >&2 || true
|
| 87 |
+
exit 1
|
| 88 |
+
fi
|
| 89 |
+
echo "[$(date)] warm start from ${WARM_SRC}/checkpoint_${NEWEST}"
|
| 90 |
+
rm -rf "${WARM_DIR:?}"/checkpoint_*
|
| 91 |
+
cp -r "${WARM_SRC}/checkpoint_${NEWEST}" "${WARM_DIR}/"
|
| 92 |
+
du -sh "${WARM_DIR}/checkpoint_${NEWEST}"
|
| 93 |
+
|
| 94 |
+
export SUDOKU_RESUME=1
|
| 95 |
+
export SUDOKU_START_STAGE=2
|
| 96 |
+
export SUDOKU_MAX_STAGE=12
|
| 97 |
+
export SUDOKU_LATENT_SLOTS=12
|
| 98 |
+
export SUDOKU_RECURRENT=1
|
| 99 |
+
export SUDOKU_BACKTRACK=0
|
| 100 |
+
export SUDOKU_CAND_SLOT_MODE=depth
|
| 101 |
+
export SUDOKU_PASSES_PER_STAGE=1
|
| 102 |
+
export SUDOKU_AUX_WEIGHT=0.0
|
| 103 |
+
export SUDOKU_LEVEL_BALANCED=0
|
| 104 |
+
export SUDOKU_DATA_CURRICULUM=none
|
| 105 |
+
# Same sampling as stage 1, rebuilt per stage: every (puzzle, instance) pair of
|
| 106 |
+
# the current stage, walked in shuffled passes, 5 sightings each.
|
| 107 |
+
export SUDOKU_INSTANCE_EPOCHS=5
|
| 108 |
+
# 100k puzzles keeps a stage at ~47k steps (~1.2 h), so all 11 stages fit in
|
| 109 |
+
# ~15 h. The full corpus would be ~22 h for a single stage.
|
| 110 |
+
export SUDOKU_INSTANCE_PUZZLES="${SUDOKU_INSTANCE_PUZZLES:-100000}"
|
| 111 |
+
# Covers 5 passes at every stage; stage 2 needs the most at 46,555 steps.
|
| 112 |
+
export SUDOKU_MIN_STAGE_STEPS=50000
|
| 113 |
+
# Calibrated on what stage 1 actually reached: mass* 0.9957, spread* 0.92.
|
| 114 |
+
export SUDOKU_PROMOTE_ACC=0.95
|
| 115 |
+
export SUDOKU_PROMOTE_SPREAD=0.88
|
| 116 |
+
export SUDOKU_PROMOTE_LOC_WAVE=0.0
|
| 117 |
+
export SUDOKU_PLATEAU_STEPS=0
|
| 118 |
+
export SUDOKU_PATIENCE=0
|
| 119 |
+
# Resume continues from the restored optimizer step (~270k), plus 11 stages at
|
| 120 |
+
# >=50k each.
|
| 121 |
+
export SUDOKU_MAX_STEPS="${SUDOKU_MAX_STEPS:-950000}"
|
| 122 |
+
export SUDOKU_EVAL_EVERY=2000
|
| 123 |
+
export SUDOKU_SAVE_EVERY=10000
|
| 124 |
+
export SUDOKU_CKPT_KEEP=2
|
| 125 |
+
export SUDOKU_LR=0.0002
|
| 126 |
+
export SUDOKU_DROPOUT=0.2
|
| 127 |
+
export SUDOKU_WD=0.005
|
| 128 |
+
export SUDOKU_TRAIN_PATH="${SCRATCH_ROOT}/sudoku-code/datasets/train_sudoku_puzzles.npy"
|
| 129 |
+
export SUDOKU_TEST_PATH="${SCRATCH_ROOT}/sudoku-code/datasets/test_sudoku_puzzles.npy"
|
| 130 |
+
export SUDOKU_TRAIN_CAND="${CAND_DIR}/train_cand_masks.npy"
|
| 131 |
+
export SUDOKU_TEST_CAND="${CAND_DIR}/test_cand_masks.npy"
|
| 132 |
+
export SUDOKU_INSTANCE_DIR="${INST_DIR}"
|
| 133 |
+
# Two jobs share the H200s, so leave more headroom than the single-job setting.
|
| 134 |
+
export XLA_PYTHON_CLIENT_MEM_FRACTION=0.45
|
| 135 |
+
|
| 136 |
+
HF_TOKEN_FILE="${HF_TOKEN_FILE:-/scratch/users/gatmiry/.hf_token}"
|
| 137 |
+
if [ -z "${HF_TOKEN:-}" ] && [ -s "${HF_TOKEN_FILE}" ]; then
|
| 138 |
+
HF_TOKEN="$(cat "${HF_TOKEN_FILE}")"
|
| 139 |
+
export HF_TOKEN
|
| 140 |
+
fi
|
| 141 |
+
export HUGGING_FACE_HUB_TOKEN="${HF_TOKEN:-}"
|
| 142 |
+
HF_PKGS="${HF_PKGS:-/scratch/users/gatmiry/hf_pkgs}"
|
| 143 |
+
TRAIN_LOG="${RUN_DIR}/logs/w12_multistage.log"
|
| 144 |
+
SYNC_LOG="${RUN_DIR}/logs/hf_sync_w12_multistage.log"
|
| 145 |
+
|
| 146 |
+
tmp_avail_kb=$(df -Pk /tmp | awk 'NR==2{print $4}')
|
| 147 |
+
echo "[$(date)] /tmp avail ${tmp_avail_kb} KB"
|
| 148 |
+
if [ "${tmp_avail_kb}" -lt 3000000 ]; then
|
| 149 |
+
echo "ERROR: /tmp has less than 3G free; refusing to start" >&2
|
| 150 |
+
exit 1
|
| 151 |
+
fi
|
| 152 |
+
|
| 153 |
+
cd "${RUN_DIR}"
|
| 154 |
+
PYTHONPATH="${HF_PKGS}${PYTHONPATH:+:${PYTHONPATH}}" ${PY} -u "${RUN_DIR}/hf_sync.py" \
|
| 155 |
+
--workdir "${LOCAL_LOG}" \
|
| 156 |
+
--log "${TRAIN_LOG}" \
|
| 157 |
+
--scratch-log "${TRAIN_LOG}" \
|
| 158 |
+
--repo Avra98/Sudoku_superposition \
|
| 159 |
+
--prefix runs/w12_multistage \
|
| 160 |
+
--login-stage "${LOGIN_STAGE}" \
|
| 161 |
+
--token-file "${HF_TOKEN_FILE}" \
|
| 162 |
+
--interval 60 \
|
| 163 |
+
> "${SYNC_LOG}" 2>&1 &
|
| 164 |
+
SYNC_PID=$!
|
| 165 |
+
|
| 166 |
+
echo "[$(date)] starting w12_multistage"
|
| 167 |
+
echo " warm start from stage-1 checkpoint ${NEWEST}, stages 2..12"
|
| 168 |
+
echo " ${SUDOKU_INSTANCE_PUZZLES} puzzles, 5 passes per stage, min ${SUDOKU_MIN_STAGE_STEPS} steps"
|
| 169 |
+
echo " promote when mass* >= ${SUDOKU_PROMOTE_ACC} and spread* >= ${SUDOKU_PROMOTE_SPREAD}"
|
| 170 |
+
echo " hf=Avra98/Sudoku_superposition/runs/w12_multistage"
|
| 171 |
+
CUDA_VISIBLE_DEVICES=0 ${PY} -u -m train.main \
|
| 172 |
+
--workdir="${LOCAL_LOG}" --exp_name="w12_multistage" \
|
| 173 |
+
--ckpt_loc="${WARM_DIR}" \
|
| 174 |
+
> "${TRAIN_LOG}" 2>&1
|
| 175 |
+
EC=$?
|
| 176 |
+
kill ${SYNC_PID} 2>/dev/null || true
|
| 177 |
+
wait ${SYNC_PID} 2>/dev/null || true
|
| 178 |
+
PYTHONPATH="${HF_PKGS}${PYTHONPATH:+:${PYTHONPATH}}" ${PY} -u "${RUN_DIR}/hf_sync.py" --once \
|
| 179 |
+
--workdir "${LOCAL_LOG}" \
|
| 180 |
+
--log "${TRAIN_LOG}" \
|
| 181 |
+
--scratch-log "${TRAIN_LOG}" \
|
| 182 |
+
--repo Avra98/Sudoku_superposition \
|
| 183 |
+
--prefix runs/w12_multistage \
|
| 184 |
+
--login-stage "${LOGIN_STAGE}" \
|
| 185 |
+
--token-file "${HF_TOKEN_FILE}" \
|
| 186 |
+
>> "${SYNC_LOG}" 2>&1 || true
|
| 187 |
+
echo "[$(date)] w12_multistage exit ${EC}"
|
| 188 |
+
exit ${EC}
|
code/wavecurriculum_run/sbatch_stage1_super.sh
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#SBATCH --partition=songmei
|
| 3 |
+
#SBATCH --nodelist=feanor
|
| 4 |
+
#SBATCH --gres=gpu:1
|
| 5 |
+
#SBATCH --cpus-per-task=8
|
| 6 |
+
#SBATCH --mem=64G
|
| 7 |
+
#SBATCH --time=72:00:00
|
| 8 |
+
#SBATCH --job-name=w1_super
|
| 9 |
+
#SBATCH --output=/scratch/users/gatmiry/llm-reasoning-logic-puzzles/sudoku-code/wavecurriculum_run/logs/slurm_%x_%j.out
|
| 10 |
+
#SBATCH --error=/scratch/users/gatmiry/llm-reasoning-logic-puzzles/sudoku-code/wavecurriculum_run/logs/slurm_%x_%j.err
|
| 11 |
+
|
| 12 |
+
# Stage-1 superposition experiment. ONE stage, no curriculum, no promotion.
|
| 13 |
+
#
|
| 14 |
+
# The point: a cell's candidate set is only visible to the model as the spread
|
| 15 |
+
# of digits it sees at that cell across repeated showings of the SAME puzzle.
|
| 16 |
+
# Random sampling showed a puzzle roughly once per 28k steps, and only one of
|
| 17 |
+
# its instances, so that spread was never presented. Here every (puzzle,
|
| 18 |
+
# instance) pair is enumerated and walked in shuffled order 5 times, so a batch
|
| 19 |
+
# holds one instance of each of 64 different puzzles (p1-i1, p2-i2, ...) and one
|
| 20 |
+
# epoch ends when every pair has appeared 5 times.
|
| 21 |
+
|
| 22 |
+
set -u
|
| 23 |
+
hostname
|
| 24 |
+
nvidia-smi -L
|
| 25 |
+
echo "[$(date)] w12_s1_super"
|
| 26 |
+
|
| 27 |
+
SCRATCH_ROOT=/scratch/users/gatmiry/llm-reasoning-logic-puzzles
|
| 28 |
+
RUN_DIR=${SCRATCH_ROOT}/sudoku-code/wavecurriculum_run
|
| 29 |
+
ENV_LOCAL=/tmp/logicpuzzles
|
| 30 |
+
CAND_DIR=/tmp/sudoku_s12
|
| 31 |
+
INST_DIR=/tmp/sudoku_superposition
|
| 32 |
+
LOCAL_LOG=/tmp/sudoku_wave_runs/w12_s1_super
|
| 33 |
+
TARBALL_GANDALF=/tmp/logicpuzzles_env.tar.gz
|
| 34 |
+
TARBALL_LOCAL=/tmp/logicpuzzles_env_${SLURM_JOB_ID}.tar.gz
|
| 35 |
+
GANDALF_INST=gandalf.berkeley.edu:/tmp/sudoku_superposition
|
| 36 |
+
GANDALF_CAND=gandalf.berkeley.edu:/tmp/sudoku_s12
|
| 37 |
+
LOGIN_STAGE=gandalf.berkeley.edu:/tmp/sudoku_hf_s1super
|
| 38 |
+
|
| 39 |
+
mkdir -p "${RUN_DIR}/logs" /tmp/sudoku_wave_runs
|
| 40 |
+
echo "[$(date)] /tmp before cleanup:"
|
| 41 |
+
df -h /tmp | tail -1
|
| 42 |
+
# Drop leftover run dirs from dead jobs; keep env + instance/mask caches.
|
| 43 |
+
rm -rf /tmp/sudoku_wave_runs
|
| 44 |
+
mkdir -p "${LOCAL_LOG}"
|
| 45 |
+
echo "[$(date)] /tmp after cleanup:"
|
| 46 |
+
df -h /tmp | tail -1
|
| 47 |
+
|
| 48 |
+
SCP_OPTS="-o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
|
| 49 |
+
[ -f "${HOME}/.ssh/id_ed25519_berkeley" ] && SCP_OPTS="${SCP_OPTS} -i ${HOME}/.ssh/id_ed25519_berkeley"
|
| 50 |
+
|
| 51 |
+
if ${ENV_LOCAL}/bin/python -u -c "import jax; assert jax.default_backend()=='gpu' or 'cuda' in str(jax.devices()[0]).lower()" 2>/dev/null; then
|
| 52 |
+
echo "[$(date)] reusing ${ENV_LOCAL}"
|
| 53 |
+
else
|
| 54 |
+
echo "[$(date)] fetching env tarball"
|
| 55 |
+
rm -rf "${ENV_LOCAL}"
|
| 56 |
+
scp ${SCP_OPTS} "gandalf.berkeley.edu:${TARBALL_GANDALF}" "${TARBALL_LOCAL}"
|
| 57 |
+
tar xzf "${TARBALL_LOCAL}" -C /tmp
|
| 58 |
+
rm -f "${TARBALL_LOCAL}"
|
| 59 |
+
fi
|
| 60 |
+
|
| 61 |
+
export PY=${ENV_LOCAL}/bin/python
|
| 62 |
+
export LD_LIBRARY_PATH=\
|
| 63 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cudnn/lib:\
|
| 64 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cublas/lib:\
|
| 65 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_runtime/lib:\
|
| 66 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_nvrtc/lib:\
|
| 67 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/nccl/lib:\
|
| 68 |
+
${LD_LIBRARY_PATH:-}
|
| 69 |
+
|
| 70 |
+
${PY} -u -c "import jax; print(jax.__version__, jax.devices(), jax.default_backend())"
|
| 71 |
+
|
| 72 |
+
# ---- Stage instance files onto node-local /tmp (scratch cannot hold 8 G) ----
|
| 73 |
+
need_inst=0
|
| 74 |
+
for f in train_assignments.npy train_starts.npy train_counts.npy \
|
| 75 |
+
test_assignments.npy test_starts.npy test_counts.npy; do
|
| 76 |
+
[ -s "${INST_DIR}/${f}" ] || need_inst=1
|
| 77 |
+
done
|
| 78 |
+
if [ "${need_inst}" = 1 ]; then
|
| 79 |
+
echo "[$(date)] pulling instances from ${GANDALF_INST}"
|
| 80 |
+
mkdir -p "${INST_DIR}"
|
| 81 |
+
rsync -a --progress -e "ssh ${SCP_OPTS}" "${GANDALF_INST}/" "${INST_DIR}/"
|
| 82 |
+
fi
|
| 83 |
+
for f in train_assignments.npy train_starts.npy train_counts.npy \
|
| 84 |
+
test_assignments.npy test_starts.npy test_counts.npy; do
|
| 85 |
+
[ -s "${INST_DIR}/${f}" ] || { echo "missing ${INST_DIR}/${f}" >&2; exit 1; }
|
| 86 |
+
done
|
| 87 |
+
|
| 88 |
+
need_cand=0
|
| 89 |
+
for f in train_cand_masks.npy test_cand_masks.npy; do
|
| 90 |
+
[ -s "${CAND_DIR}/${f}" ] || need_cand=1
|
| 91 |
+
done
|
| 92 |
+
if [ "${need_cand}" = 1 ]; then
|
| 93 |
+
echo "[$(date)] pulling s12 masks from ${GANDALF_CAND}"
|
| 94 |
+
mkdir -p "${CAND_DIR}"
|
| 95 |
+
rsync -a -e "ssh ${SCP_OPTS}" "${GANDALF_CAND}/" "${CAND_DIR}/" || true
|
| 96 |
+
fi
|
| 97 |
+
for f in train_cand_masks.npy test_cand_masks.npy; do
|
| 98 |
+
[ -s "${CAND_DIR}/${f}" ] || { echo "missing ${CAND_DIR}/${f}" >&2; exit 1; }
|
| 99 |
+
done
|
| 100 |
+
|
| 101 |
+
# ---- Stage 1 only: start == max, so the curriculum can never promote ----
|
| 102 |
+
export SUDOKU_RESUME=0
|
| 103 |
+
export SUDOKU_START_STAGE=1
|
| 104 |
+
export SUDOKU_MAX_STAGE=1
|
| 105 |
+
export SUDOKU_LATENT_SLOTS=12
|
| 106 |
+
export SUDOKU_RECURRENT=1
|
| 107 |
+
export SUDOKU_BACKTRACK=0
|
| 108 |
+
export SUDOKU_CAND_SLOT_MODE=depth
|
| 109 |
+
export SUDOKU_PASSES_PER_STAGE=1
|
| 110 |
+
export SUDOKU_AUX_WEIGHT=0.0
|
| 111 |
+
export SUDOKU_LEVEL_BALANCED=0
|
| 112 |
+
export SUDOKU_DATA_CURRICULUM=none
|
| 113 |
+
# One epoch = every (puzzle, instance) pair of stage 1 seen at least 5 times.
|
| 114 |
+
# 1,804,462 puzzles x 4-9 stored instances = 11,336,803 pairs.
|
| 115 |
+
export SUDOKU_INSTANCE_EPOCHS=5
|
| 116 |
+
export SUDOKU_INSTANCE_PUZZLES="${SUDOKU_INSTANCE_PUZZLES:-0}"
|
| 117 |
+
# steps = 5 * 11,336,803 / 64 = 885,688, rounded up so the epoch completes.
|
| 118 |
+
export SUDOKU_MAX_STEPS="${SUDOKU_MAX_STEPS:-886000}"
|
| 119 |
+
export SUDOKU_EVAL_EVERY=2000
|
| 120 |
+
export SUDOKU_SAVE_EVERY=10000
|
| 121 |
+
export SUDOKU_CKPT_KEEP=2
|
| 122 |
+
export SUDOKU_LR=0.0002
|
| 123 |
+
export SUDOKU_DROPOUT=0.2
|
| 124 |
+
export SUDOKU_WD=0.005
|
| 125 |
+
export SUDOKU_TRAIN_PATH="${SCRATCH_ROOT}/sudoku-code/datasets/train_sudoku_puzzles.npy"
|
| 126 |
+
export SUDOKU_TEST_PATH="${SCRATCH_ROOT}/sudoku-code/datasets/test_sudoku_puzzles.npy"
|
| 127 |
+
export SUDOKU_TRAIN_CAND="${CAND_DIR}/train_cand_masks.npy"
|
| 128 |
+
export SUDOKU_TEST_CAND="${CAND_DIR}/test_cand_masks.npy"
|
| 129 |
+
export SUDOKU_INSTANCE_DIR="${INST_DIR}"
|
| 130 |
+
# Leave a little H200 headroom so eval + sidecar tar do not OOM the node.
|
| 131 |
+
export XLA_PYTHON_CLIENT_MEM_FRACTION=0.85
|
| 132 |
+
|
| 133 |
+
HF_TOKEN_FILE="${HF_TOKEN_FILE:-/scratch/users/gatmiry/.hf_token}"
|
| 134 |
+
if [ -z "${HF_TOKEN:-}" ] && [ -s "${HF_TOKEN_FILE}" ]; then
|
| 135 |
+
HF_TOKEN="$(cat "${HF_TOKEN_FILE}")"
|
| 136 |
+
export HF_TOKEN
|
| 137 |
+
fi
|
| 138 |
+
export HUGGING_FACE_HUB_TOKEN="${HF_TOKEN:-}"
|
| 139 |
+
HF_PKGS="${HF_PKGS:-/scratch/users/gatmiry/hf_pkgs}"
|
| 140 |
+
TRAIN_LOG="${RUN_DIR}/logs/w12_s1_super.log"
|
| 141 |
+
SYNC_LOG="${RUN_DIR}/logs/hf_sync_w12_s1_super.log"
|
| 142 |
+
|
| 143 |
+
# Need room for 2 rolling ckpts (~1 GB) plus a tar in flight.
|
| 144 |
+
tmp_avail_kb=$(df -Pk /tmp | awk 'NR==2{print $4}')
|
| 145 |
+
echo "[$(date)] /tmp avail ${tmp_avail_kb} KB"
|
| 146 |
+
if [ "${tmp_avail_kb}" -lt 3000000 ]; then
|
| 147 |
+
echo "ERROR: /tmp has less than 3G free; refusing to start" >&2
|
| 148 |
+
df -h /tmp
|
| 149 |
+
du -sh /tmp/* 2>/dev/null | sort -h | tail -20 >&2 || true
|
| 150 |
+
exit 1
|
| 151 |
+
fi
|
| 152 |
+
|
| 153 |
+
cd "${RUN_DIR}"
|
| 154 |
+
PYTHONPATH="${HF_PKGS}${PYTHONPATH:+:${PYTHONPATH}}" ${PY} -u "${RUN_DIR}/hf_sync.py" \
|
| 155 |
+
--workdir "${LOCAL_LOG}" \
|
| 156 |
+
--log "${TRAIN_LOG}" \
|
| 157 |
+
--scratch-log "${TRAIN_LOG}" \
|
| 158 |
+
--repo Avra98/Sudoku_superposition \
|
| 159 |
+
--prefix runs/w12_s1_super \
|
| 160 |
+
--login-stage "${LOGIN_STAGE}" \
|
| 161 |
+
--token-file "${HF_TOKEN_FILE}" \
|
| 162 |
+
--interval 60 \
|
| 163 |
+
> "${SYNC_LOG}" 2>&1 &
|
| 164 |
+
SYNC_PID=$!
|
| 165 |
+
|
| 166 |
+
echo "[$(date)] starting w12_s1_super from scratch"
|
| 167 |
+
echo " stage pinned to 1 (start=max=1), 1 latent pass, aux=0"
|
| 168 |
+
echo " 1 epoch = every (puzzle, instance) pair seen 5x"
|
| 169 |
+
echo " workdir=${LOCAL_LOG} log=${TRAIN_LOG}"
|
| 170 |
+
echo " hf=Avra98/Sudoku_superposition/runs/w12_s1_super"
|
| 171 |
+
CUDA_VISIBLE_DEVICES=0 ${PY} -u -m train.main \
|
| 172 |
+
--workdir="${LOCAL_LOG}" --exp_name="w12_s1_super" \
|
| 173 |
+
> "${TRAIN_LOG}" 2>&1
|
| 174 |
+
EC=$?
|
| 175 |
+
kill ${SYNC_PID} 2>/dev/null || true
|
| 176 |
+
wait ${SYNC_PID} 2>/dev/null || true
|
| 177 |
+
PYTHONPATH="${HF_PKGS}${PYTHONPATH:+:${PYTHONPATH}}" ${PY} -u "${RUN_DIR}/hf_sync.py" --once \
|
| 178 |
+
--workdir "${LOCAL_LOG}" \
|
| 179 |
+
--log "${TRAIN_LOG}" \
|
| 180 |
+
--scratch-log "${TRAIN_LOG}" \
|
| 181 |
+
--repo Avra98/Sudoku_superposition \
|
| 182 |
+
--prefix runs/w12_s1_super \
|
| 183 |
+
--login-stage "${LOGIN_STAGE}" \
|
| 184 |
+
--token-file "${HF_TOKEN_FILE}" \
|
| 185 |
+
>> "${SYNC_LOG}" 2>&1 || true
|
| 186 |
+
echo "[$(date)] w12_s1_super exit ${EC}"
|
| 187 |
+
exit ${EC}
|
code/wavecurriculum_run/sbatch_wave12.sh
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#SBATCH --partition=songmei
|
| 3 |
+
#SBATCH --nodelist=feanor
|
| 4 |
+
#SBATCH --gres=gpu:1
|
| 5 |
+
#SBATCH --cpus-per-task=8
|
| 6 |
+
#SBATCH --mem=64G
|
| 7 |
+
#SBATCH --time=72:00:00
|
| 8 |
+
#SBATCH --job-name=wave12
|
| 9 |
+
#SBATCH --output=/scratch/users/gatmiry/llm-reasoning-logic-puzzles/sudoku-code/wavecurriculum_run/logs/slurm_%x_%j.out
|
| 10 |
+
#SBATCH --error=/scratch/users/gatmiry/llm-reasoning-logic-puzzles/sudoku-code/wavecurriculum_run/logs/slurm_%x_%j.err
|
| 11 |
+
|
| 12 |
+
# Wave-depth curriculum, 12 stages.
|
| 13 |
+
#
|
| 14 |
+
# The curriculum axis is PROPAGATION DEPTH, not puzzle difficulty. Each puzzle's
|
| 15 |
+
# ~25 solver waves are subsampled to 12 evenly spaced candidate snapshots
|
| 16 |
+
# (staged_candidate_gen.py --stages 12, last snapshot = the unique solution).
|
| 17 |
+
# Curriculum stage t means: t recurrence passes, latent slots 1..t active, wave
|
| 18 |
+
# snapshots 1..t supervised. Promotion adds exactly one wave block.
|
| 19 |
+
#
|
| 20 |
+
# Difficulty is NOT gated: every puzzle is available from step 0, drawn
|
| 21 |
+
# uniformly from the corpus. The old level<=stage+2 filter is gone -- the
|
| 22 |
+
# difficulty tag has only 6 values (so it cannot express a 12-step ladder), is
|
| 23 |
+
# uncorrelated with puzzle size (r=-0.003 vs empty cells), and explains only
|
| 24 |
+
# ~19% of the variance in solver round count.
|
| 25 |
+
#
|
| 26 |
+
# Promotion is plateau-driven: a stage graduates when the candidate-set accuracy
|
| 27 |
+
# at its deepest slot stops improving, scored on cells that CHANGED from the
|
| 28 |
+
# previous snapshot. Unchanged cells are copies of slot j-1 and stay correct for
|
| 29 |
+
# a head that learned nothing, so they cannot signal acquisition. The accuracy
|
| 30 |
+
# threshold is a fast path and the patience cap prevents a stuck stage stalling.
|
| 31 |
+
#
|
| 32 |
+
# Submit one job per arm:
|
| 33 |
+
# sbatch --job-name=w12_flat --export=ALL,ARM=flat sbatch_wave12.sh
|
| 34 |
+
# sbatch --job-name=w12_datacur --export=ALL,ARM=datacur sbatch_wave12.sh
|
| 35 |
+
# sbatch --job-name=w12_latent --export=ALL,ARM=latent sbatch_wave12.sh
|
| 36 |
+
# sbatch --job-name=w12_latent_bt --export=ALL,ARM=latent_bt sbatch_wave12.sh
|
| 37 |
+
#
|
| 38 |
+
# ARM:
|
| 39 |
+
# flat K=0, no curriculum at all: pure control.
|
| 40 |
+
# datacur K=0 with the 12-stage round-count DATA curriculum. Stage t admits
|
| 41 |
+
# only puzzles in the first t round-count bins, so the propagation
|
| 42 |
+
# ladder is expressed in the puzzle POOL rather than in latent
|
| 43 |
+
# supervision. This is the no-latent curriculum arm: same axis as
|
| 44 |
+
# the latent arms, no latent tokens. Promotion is gated on accuracy
|
| 45 |
+
# over the newest round-bin.
|
| 46 |
+
# latent K=12 latent chain, 12-stage wave supervision, plateau promotion.
|
| 47 |
+
# latent_bt as latent, plus adaptive backtracking keyed on DEPTH: a repair
|
| 48 |
+
# replays num_passes=t when snapshot t's accuracy regresses below
|
| 49 |
+
# its graduation value.
|
| 50 |
+
#
|
| 51 |
+
# From scratch is forced, not chosen: pos_embeddings is exactly (3*81+K, emb_dim),
|
| 52 |
+
# so no 6-slot checkpoint can restore into a 12-slot model.
|
| 53 |
+
#
|
| 54 |
+
# The 12-stage masks live on feanor's node-local /tmp (sbatch_gen_s12_masks.sh)
|
| 55 |
+
# because the 20 G scratch quota has ~2 G free and the train masks are 3.5 G.
|
| 56 |
+
|
| 57 |
+
set -u
|
| 58 |
+
hostname
|
| 59 |
+
nvidia-smi -L
|
| 60 |
+
echo "[$(date)] ARM=${ARM:-unset}"
|
| 61 |
+
|
| 62 |
+
ARM="${ARM:?set ARM=flat|datacur|latent|latent_bt via --export=ALL,ARM=...}"
|
| 63 |
+
NAME="w12_${ARM}"
|
| 64 |
+
|
| 65 |
+
SCRATCH_ROOT=/scratch/users/gatmiry/llm-reasoning-logic-puzzles
|
| 66 |
+
RUN_DIR=${SCRATCH_ROOT}/sudoku-code/wavecurriculum_run
|
| 67 |
+
ENV_LOCAL=/tmp/logicpuzzles
|
| 68 |
+
CAND_DIR=/tmp/sudoku_s12
|
| 69 |
+
LOCAL_LOG=/tmp/sudoku_wave_runs/${NAME}
|
| 70 |
+
TARBALL_GANDALF=/tmp/logicpuzzles_env.tar.gz
|
| 71 |
+
TARBALL_LOCAL=/tmp/logicpuzzles_env_${SLURM_JOB_ID}.tar.gz
|
| 72 |
+
|
| 73 |
+
mkdir -p "${RUN_DIR}/logs" "${LOCAL_LOG}" /tmp/sudoku_wave_runs
|
| 74 |
+
|
| 75 |
+
if ${ENV_LOCAL}/bin/python -u -c "import jax; assert jax.default_backend()=='gpu' or 'cuda' in str(jax.devices()[0]).lower()" 2>/dev/null; then
|
| 76 |
+
echo "[$(date)] reusing ${ENV_LOCAL}"
|
| 77 |
+
else
|
| 78 |
+
echo "[$(date)] fetching env tarball"
|
| 79 |
+
rm -rf "${ENV_LOCAL}"
|
| 80 |
+
SCP_OPTS="-o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
|
| 81 |
+
[ -f "${HOME}/.ssh/id_ed25519_berkeley" ] && SCP_OPTS="${SCP_OPTS} -i ${HOME}/.ssh/id_ed25519_berkeley"
|
| 82 |
+
scp ${SCP_OPTS} "gandalf.berkeley.edu:${TARBALL_GANDALF}" "${TARBALL_LOCAL}"
|
| 83 |
+
tar xzf "${TARBALL_LOCAL}" -C /tmp
|
| 84 |
+
rm -f "${TARBALL_LOCAL}"
|
| 85 |
+
fi
|
| 86 |
+
|
| 87 |
+
export PY=${ENV_LOCAL}/bin/python
|
| 88 |
+
export LD_LIBRARY_PATH=\
|
| 89 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cudnn/lib:\
|
| 90 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cublas/lib:\
|
| 91 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_runtime/lib:\
|
| 92 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_nvrtc/lib:\
|
| 93 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/nccl/lib:\
|
| 94 |
+
${LD_LIBRARY_PATH:-}
|
| 95 |
+
|
| 96 |
+
${PY} -u -c "import jax; print(jax.__version__, jax.devices(), jax.default_backend())"
|
| 97 |
+
|
| 98 |
+
# ---- Shared recipe: identical across arms ----
|
| 99 |
+
export SUDOKU_RESUME=0
|
| 100 |
+
export SUDOKU_START_STAGE=1
|
| 101 |
+
export SUDOKU_MAX_STAGE="${SUDOKU_MAX_STAGE:-12}"
|
| 102 |
+
# Plateau promotion. A stage graduates when its frontier-depth accuracy has not
|
| 103 |
+
# gained 0.005 for 20k steps; PATIENCE is only a hard ceiling. This replaces the
|
| 104 |
+
# old fixed 15k timer, under which every promotion fired on patience at 0.32-0.46
|
| 105 |
+
# accuracy and the 0.70 threshold was dead code.
|
| 106 |
+
export SUDOKU_PLATEAU_STEPS=20000
|
| 107 |
+
export SUDOKU_PLATEAU_DELTA=0.005
|
| 108 |
+
export SUDOKU_PATIENCE=80000
|
| 109 |
+
export SUDOKU_MIN_STAGE_STEPS=8000
|
| 110 |
+
export SUDOKU_PROMOTE_ACC=0.90
|
| 111 |
+
# Matches the flat reference run (which reached level-3 acc 0.945 at ~800k), so
|
| 112 |
+
# the arms are comparable to it at equal steps rather than stopping at 250k.
|
| 113 |
+
export SUDOKU_MAX_STEPS="${SUDOKU_MAX_STEPS:-800000}"
|
| 114 |
+
# Difficulty tag selects nothing: uniform over the corpus (~68% level 3).
|
| 115 |
+
export SUDOKU_LEVEL_BALANCED=0
|
| 116 |
+
export SUDOKU_EVAL_EVERY=2000
|
| 117 |
+
export SUDOKU_SAVE_EVERY=10000
|
| 118 |
+
export SUDOKU_CKPT_KEEP=3
|
| 119 |
+
export SUDOKU_LR=0.0002
|
| 120 |
+
export SUDOKU_DROPOUT=0.2
|
| 121 |
+
export SUDOKU_WD=0.005
|
| 122 |
+
export SUDOKU_TRAIN_PATH="../datasets/train_sudoku_puzzles.npy"
|
| 123 |
+
export SUDOKU_TEST_PATH="../datasets/test_sudoku_puzzles.npy"
|
| 124 |
+
export XLA_PYTHON_CLIENT_MEM_FRACTION=0.9
|
| 125 |
+
|
| 126 |
+
# ---- Per-arm knobs ----
|
| 127 |
+
export SUDOKU_BACKTRACK=0
|
| 128 |
+
case "${ARM}" in
|
| 129 |
+
flat)
|
| 130 |
+
# No latents: no candidate head, no depth ladder. Masks left unset.
|
| 131 |
+
export SUDOKU_LATENT_SLOTS=0
|
| 132 |
+
export SUDOKU_RECURRENT=0
|
| 133 |
+
export SUDOKU_AUX_WEIGHT=0.0
|
| 134 |
+
export SUDOKU_TRAIN_CAND=""
|
| 135 |
+
export SUDOKU_TEST_CAND=""
|
| 136 |
+
;;
|
| 137 |
+
datacur)
|
| 138 |
+
# No latents, but a real 12-stage curriculum over the puzzle pool, ordered
|
| 139 |
+
# by solver round count. Candidate masks stay unset (nothing to supervise);
|
| 140 |
+
# only the meta files are needed, for the round counts.
|
| 141 |
+
export SUDOKU_LATENT_SLOTS=0
|
| 142 |
+
export SUDOKU_RECURRENT=0
|
| 143 |
+
export SUDOKU_AUX_WEIGHT=0.0
|
| 144 |
+
export SUDOKU_TRAIN_CAND=""
|
| 145 |
+
export SUDOKU_TEST_CAND=""
|
| 146 |
+
export SUDOKU_DATA_CURRICULUM=rounds
|
| 147 |
+
export SUDOKU_TRAIN_META="${CAND_DIR}/train_meta.npy"
|
| 148 |
+
export SUDOKU_TEST_META="${CAND_DIR}/test_meta.npy"
|
| 149 |
+
for f in "${SUDOKU_TRAIN_META}" "${SUDOKU_TEST_META}"; do
|
| 150 |
+
[ -s "${f}" ] || { echo "missing meta ${f}; run sbatch_gen_s12_masks.sh on this node" >&2; exit 1; }
|
| 151 |
+
done
|
| 152 |
+
;;
|
| 153 |
+
latent|latent_bt)
|
| 154 |
+
export SUDOKU_LATENT_SLOTS=12
|
| 155 |
+
export SUDOKU_RECURRENT=1
|
| 156 |
+
export SUDOKU_AUX_WEIGHT=1.0
|
| 157 |
+
export SUDOKU_CAND_SLOT_MODE=depth # k = num_passes, slot j -> snapshot j
|
| 158 |
+
export SUDOKU_PASSES_PER_STAGE=1 # stage t -> depth t, 12 stages -> 12 slots
|
| 159 |
+
export SUDOKU_CAND_DELTA_BG=0.25 # down-weight cells copied from slot j-1
|
| 160 |
+
export SUDOKU_TRAIN_CAND="${CAND_DIR}/train_cand_masks.npy"
|
| 161 |
+
export SUDOKU_TEST_CAND="${CAND_DIR}/test_cand_masks.npy"
|
| 162 |
+
for f in "${SUDOKU_TRAIN_CAND}" "${SUDOKU_TEST_CAND}"; do
|
| 163 |
+
[ -s "${f}" ] || { echo "missing masks ${f}; run sbatch_gen_s12_masks.sh on this node" >&2; exit 1; }
|
| 164 |
+
done
|
| 165 |
+
;;
|
| 166 |
+
*) echo "unknown ARM '${ARM}'" >&2; exit 1 ;;
|
| 167 |
+
esac
|
| 168 |
+
|
| 169 |
+
if [ "${ARM}" = "latent_bt" ]; then
|
| 170 |
+
# Adaptive replay keyed on depth: deficits are measured on snapshot t's
|
| 171 |
+
# candidate-set accuracy, and a repair trains at num_passes=t on the same
|
| 172 |
+
# full-corpus batch distribution the frontier uses.
|
| 173 |
+
export SUDOKU_BACKTRACK=1
|
| 174 |
+
export SUDOKU_BACKTRACK_MODE=adaptive
|
| 175 |
+
export SUDOKU_BACKTRACK_MARGIN=0.03
|
| 176 |
+
export SUDOKU_BACKTRACK_MAX_REPAIR_STEPS=4000
|
| 177 |
+
export SUDOKU_BACKTRACK_MIN_FRONTIER_STEPS=8000
|
| 178 |
+
export SUDOKU_BACKTRACK_MAX_REPAIR_FRACTION=0.25
|
| 179 |
+
export SUDOKU_BACKTRACK_GRAD_DECAY=0.05
|
| 180 |
+
export SUDOKU_BACKTRACK_FRONTIER_MIX=1
|
| 181 |
+
fi
|
| 182 |
+
|
| 183 |
+
cd "${RUN_DIR}"
|
| 184 |
+
# Log-only periodic sync: checkpoints stay on /tmp (20 G scratch quota).
|
| 185 |
+
(
|
| 186 |
+
while true; do sleep 900
|
| 187 |
+
rsync -a "${LOCAL_LOG}.log" "${RUN_DIR}/logs/${NAME}.log" 2>/dev/null || true
|
| 188 |
+
done
|
| 189 |
+
) &
|
| 190 |
+
SYNC_PID=$!
|
| 191 |
+
|
| 192 |
+
echo "[$(date)] starting ${NAME} from scratch"
|
| 193 |
+
echo " K=${SUDOKU_LATENT_SLOTS} recurrent=${SUDOKU_RECURRENT} bt=${SUDOKU_BACKTRACK}"
|
| 194 |
+
echo " max_stage=${SUDOKU_MAX_STAGE} plateau=${SUDOKU_PLATEAU_STEPS} patience=${SUDOKU_PATIENCE}"
|
| 195 |
+
echo " slot_mode=${SUDOKU_CAND_SLOT_MODE:-n/a} pps=${SUDOKU_PASSES_PER_STAGE:-n/a} delta_bg=${SUDOKU_CAND_DELTA_BG:-n/a}"
|
| 196 |
+
CUDA_VISIBLE_DEVICES=0 ${PY} -u -m train.main \
|
| 197 |
+
--workdir="${LOCAL_LOG}" --exp_name="${NAME}" \
|
| 198 |
+
> "${LOCAL_LOG}.log" 2>&1
|
| 199 |
+
EC=$?
|
| 200 |
+
kill ${SYNC_PID} 2>/dev/null || true
|
| 201 |
+
rsync -a "${LOCAL_LOG}.log" "${RUN_DIR}/logs/${NAME}.log" 2>/dev/null || true
|
| 202 |
+
echo "[$(date)] ${NAME} exit ${EC}"
|
| 203 |
+
exit ${EC}
|
code/wavecurriculum_run/show_val_example.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Worked example: one cell with candidate set S = {1,2,4}, four model behaviours.
|
| 2 |
+
|
| 3 |
+
Shows that excess = log(1/mass) + KL(Uniform(S) || p/mass) separates "support
|
| 4 |
+
leaks outside S" from "support inside S is not uniform".
|
| 5 |
+
"""
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
import types
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
for name in ("jax", "jax.numpy", "flax", "flax.training",
|
| 13 |
+
"flax.training.common_utils", "train.model"):
|
| 14 |
+
sys.modules.setdefault(name, types.ModuleType(name))
|
| 15 |
+
sys.modules["jax"].numpy = sys.modules["jax.numpy"]
|
| 16 |
+
sys.modules["flax"].training = sys.modules["flax.training"]
|
| 17 |
+
sys.modules["flax.training"].common_utils = sys.modules[
|
| 18 |
+
"flax.training.common_utils"]
|
| 19 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 20 |
+
from train.evaluater import _cand_value_stats
|
| 21 |
+
|
| 22 |
+
CAND = [1, 2, 4] # digits
|
| 23 |
+
BITS = int(sum(1 << (d - 1) for d in CAND))
|
| 24 |
+
|
| 25 |
+
cases = [
|
| 26 |
+
("A perfect superposition ", {1: 1 / 3, 2: 1 / 3, 4: 1 / 3}),
|
| 27 |
+
("B uniform over all 9 ", {d: 1 / 9 for d in range(1, 10)}),
|
| 28 |
+
("C in S but collapsed on 4 ", {1: 0.05, 2: 0.05, 4: 0.90}),
|
| 29 |
+
("D leaks AND collapsed ", {1: 0.05, 2: 0.05, 4: 0.50,
|
| 30 |
+
7: 0.20, 8: 0.20}),
|
| 31 |
+
# A softmax never emits an exact zero, so keep a little mass on S here;
|
| 32 |
+
# with p(d) == 0 for every candidate the CE is +inf by definition.
|
| 33 |
+
("E almost all mass outside S", {1: 0.005, 2: 0.005, 4: 0.005, 7: 0.985}),
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
hdr = (f"{'case':<28}{'mass':>7}{'out':>8}{'leak':>9}{'kl':>9}"
|
| 37 |
+
f"{'excess':>9}{'spread':>8}{'score':>8}")
|
| 38 |
+
print(f"candidate set S = {{{','.join(map(str, CAND))}}}, |S| = {len(CAND)}, "
|
| 39 |
+
f"log|S| = {np.log(len(CAND)):.4f}")
|
| 40 |
+
print(hdr)
|
| 41 |
+
print("-" * len(hdr))
|
| 42 |
+
for label, pmap in cases:
|
| 43 |
+
p = np.zeros(9)
|
| 44 |
+
for d, v in pmap.items():
|
| 45 |
+
p[d - 1] = v
|
| 46 |
+
assert abs(p.sum() - 1.0) < 1e-9, (label, p.sum())
|
| 47 |
+
logp = np.log(np.maximum(p, 1e-300))[None, :]
|
| 48 |
+
st = _cand_value_stats(logp, np.array([BITS]))
|
| 49 |
+
excess = st["ce"] - st["floor"]
|
| 50 |
+
mass = st["mass"]
|
| 51 |
+
leak = np.log(1.0 / max(mass, 1e-300))
|
| 52 |
+
kl = st["kl_multi"]
|
| 53 |
+
# the identity, recomputed independently of the code
|
| 54 |
+
assert abs(excess - (leak + kl)) < 1e-8, (label, excess, leak + kl)
|
| 55 |
+
print(f"{label:<28}{mass:>7.3f}{1 - mass:>8.3f}{leak:>9.3f}{kl:>9.3f}"
|
| 56 |
+
f"{excess:>9.3f}{st['spread']:>8.3f}{np.exp(-excess):>8.3f}")
|
| 57 |
+
|
| 58 |
+
print()
|
| 59 |
+
print("out = val_acc = 1 - mass = P(digit outside S)")
|
| 60 |
+
print("leak = log(1/mass) -> 0 when no support outside S")
|
| 61 |
+
print("kl = KL(Unif(S)||p/m) -> 0 when support inside S is uniform")
|
| 62 |
+
print("excess = leak + kl -> 0 iff BOTH; this is the promotion signal")
|
| 63 |
+
print("score = exp(-excess) -> 1.0 iff both; compared to SUDOKU_PROMOTE_ACC")
|
code/wavecurriculum_run/test_instance_epochs.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Check the (puzzle, instance) epoch enumeration on the real stage-1 counts.
|
| 2 |
+
|
| 3 |
+
Verifies the property the experiment rests on: after E epochs every instance of
|
| 4 |
+
every puzzle has been used exactly E times, and a batch holds distinct puzzles
|
| 5 |
+
(one instance of a given puzzle per batch).
|
| 6 |
+
"""
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def build_pairs(starts, counts, stage, n_puz):
|
| 11 |
+
"""Same expansion as SudokuDataset._build_instance_epoch_list."""
|
| 12 |
+
c = np.asarray(counts[:n_puz, stage]).astype(np.int64)
|
| 13 |
+
s = np.asarray(starts[:n_puz, stage]).astype(np.int64)
|
| 14 |
+
total = int(c.sum())
|
| 15 |
+
puzzle_ids = np.repeat(np.arange(n_puz, dtype=np.int64), c)
|
| 16 |
+
offsets = np.arange(total, dtype=np.int64) - np.repeat(np.cumsum(c) - c, c)
|
| 17 |
+
rows = np.repeat(s, c) + offsets
|
| 18 |
+
return puzzle_ids, rows, c
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
d = "/tmp/sudoku_superposition"
|
| 23 |
+
starts = np.load(f"{d}/train_starts.npy")
|
| 24 |
+
counts = np.load(f"{d}/train_counts.npy")
|
| 25 |
+
stage, n_puz, epochs, bs = 0, 20000, 5, 64
|
| 26 |
+
|
| 27 |
+
pids, rows, c = build_pairs(starts, counts, stage, n_puz)
|
| 28 |
+
total = len(rows)
|
| 29 |
+
print(f"stage {stage + 1}: {n_puz} puzzles -> {total} pairs, "
|
| 30 |
+
f"{c.min()}-{c.max()} instances/puzzle (mean {c.mean():.2f})")
|
| 31 |
+
|
| 32 |
+
# 1. rows are exactly each puzzle's contiguous assignment block, no repeats
|
| 33 |
+
assert total == int(c.sum())
|
| 34 |
+
assert len(np.unique(rows)) == total, "duplicate assignment rows"
|
| 35 |
+
for p in (0, 1, 7, n_puz - 1):
|
| 36 |
+
want = np.arange(starts[p, stage], starts[p, stage] + counts[p, stage])
|
| 37 |
+
got = np.sort(rows[pids == p])
|
| 38 |
+
assert np.array_equal(got, want), (p, got, want)
|
| 39 |
+
print("rows match each puzzle's assignment block OK")
|
| 40 |
+
|
| 41 |
+
# 2. every instance seen exactly `epochs` times
|
| 42 |
+
rng = np.random.RandomState(0)
|
| 43 |
+
seen = np.zeros(total, dtype=np.int32)
|
| 44 |
+
order = np.arange(total)
|
| 45 |
+
batch_dup = 0
|
| 46 |
+
n_batches = 0
|
| 47 |
+
for _ in range(epochs):
|
| 48 |
+
rng.shuffle(order)
|
| 49 |
+
seen[order] += 1
|
| 50 |
+
for b in range(0, total - bs, bs):
|
| 51 |
+
sl = pids[order[b:b + bs]]
|
| 52 |
+
batch_dup += bs - len(np.unique(sl))
|
| 53 |
+
n_batches += 1
|
| 54 |
+
assert seen.min() == seen.max() == epochs, (seen.min(), seen.max())
|
| 55 |
+
print(f"every instance seen exactly {epochs}x OK")
|
| 56 |
+
|
| 57 |
+
# 3. per-puzzle: all of its instances covered, each `epochs` times
|
| 58 |
+
per_puzzle = np.bincount(pids, weights=seen, minlength=n_puz)
|
| 59 |
+
assert np.array_equal(per_puzzle, epochs * c)
|
| 60 |
+
print("every puzzle: all instances x epochs OK")
|
| 61 |
+
|
| 62 |
+
# 4. batches hold distinct puzzles
|
| 63 |
+
print(f"same-puzzle collisions in a batch: {batch_dup} over "
|
| 64 |
+
f"{n_batches} batches ({batch_dup / (n_batches * bs) * 100:.3f}% "
|
| 65 |
+
f"of slots)")
|
| 66 |
+
|
| 67 |
+
# 5. averaged over the epochs, the target at a cell IS the candidate set
|
| 68 |
+
A = np.load(f"{d}/train_assignments.npy", mmap_mode="r")
|
| 69 |
+
p = 3
|
| 70 |
+
blk = np.asarray(A[starts[p, stage]:starts[p, stage] + counts[p, stage]])
|
| 71 |
+
multi = [c_ for c_ in range(81) if len(np.unique(blk[:, c_])) > 1]
|
| 72 |
+
print(f"\npuzzle {p}: {counts[p, stage]} instances, "
|
| 73 |
+
f"{len(multi)} cells whose digit varies across instances")
|
| 74 |
+
for cell in multi[:5]:
|
| 75 |
+
vals, cnt = np.unique(blk[:, cell], return_counts=True)
|
| 76 |
+
emp = ", ".join(f"{v}:{n}/{len(blk)}" for v, n in zip(vals, cnt))
|
| 77 |
+
print(f" cell (r{cell // 9}, c{cell % 9}) digits {emp}")
|
| 78 |
+
print("\nthis empirical spread over instances is exactly what the model "
|
| 79 |
+
"must reproduce; it only sees it if all instances are visited")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
main()
|
code/wavecurriculum_run/test_new_metrics.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Numerical checks for the new location and value metrics.
|
| 2 |
+
|
| 3 |
+
Run: python wavecurriculum_run/test_new_metrics.py
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
import types
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
# The helpers under test are pure numpy, so stub the heavy imports at the top of
|
| 12 |
+
# evaluater.py to keep this runnable on any node without jax installed.
|
| 13 |
+
for name in ("jax", "jax.numpy", "flax", "flax.training",
|
| 14 |
+
"flax.training.common_utils", "train.model"):
|
| 15 |
+
sys.modules.setdefault(name, types.ModuleType(name))
|
| 16 |
+
sys.modules["jax"].numpy = sys.modules["jax.numpy"]
|
| 17 |
+
sys.modules["flax"].training = sys.modules["flax.training"]
|
| 18 |
+
sys.modules["flax.training"].common_utils = sys.modules[
|
| 19 |
+
"flax.training.common_utils"]
|
| 20 |
+
|
| 21 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 22 |
+
from train.evaluater import (_cand_value_stats, _wave_order_score, _lcs_len)
|
| 23 |
+
|
| 24 |
+
fails = []
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def check(name, got, want, tol=1e-6):
|
| 28 |
+
ok = abs(float(got) - float(want)) <= tol
|
| 29 |
+
print(f"{'PASS' if ok else 'FAIL'} {name}: got {got:.6f} want {want:.6f}")
|
| 30 |
+
if not ok:
|
| 31 |
+
fails.append(name)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def logp_from_probs(p):
|
| 35 |
+
p = np.asarray(p, dtype=np.float64)
|
| 36 |
+
return np.log(np.maximum(p, 1e-300))
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
BITS_123 = 0b000000111 # candidates {1,2,3}
|
| 40 |
+
BITS_5 = 0b000010000 # candidate {5}
|
| 41 |
+
|
| 42 |
+
# ---- val_excess == 0 exactly at uniform over the candidate set ----
|
| 43 |
+
p = np.zeros((1, 9)); p[0, :3] = 1.0 / 3.0
|
| 44 |
+
st = _cand_value_stats(logp_from_probs(p), np.array([BITS_123]))
|
| 45 |
+
check("uniform-over-S excess", st["ce"] - st["floor"], 0.0)
|
| 46 |
+
check("uniform-over-S mass", st["mass"], 1.0)
|
| 47 |
+
check("uniform-over-S spread", st["spread"], 1.0)
|
| 48 |
+
check("uniform-over-S floor==log3", st["floor"], np.log(3))
|
| 49 |
+
|
| 50 |
+
# ---- collapse onto one candidate: excess > 0, spread -> 0 ----
|
| 51 |
+
p = np.zeros((1, 9)); p[0, 0] = 0.98; p[0, 1] = 0.01; p[0, 2] = 0.01
|
| 52 |
+
st = _cand_value_stats(logp_from_probs(p), np.array([BITS_123]))
|
| 53 |
+
excess = st["ce"] - st["floor"]
|
| 54 |
+
print(f" collapsed excess = {excess:.4f} (want >> 0), "
|
| 55 |
+
f"spread = {st['spread']:.4f} (want ~0)")
|
| 56 |
+
if not excess > 1.0:
|
| 57 |
+
fails.append("collapsed excess > 1")
|
| 58 |
+
if not st["spread"] < 0.2:
|
| 59 |
+
fails.append("collapsed spread < 0.2")
|
| 60 |
+
|
| 61 |
+
# ---- leakage outside the set lowers mass and raises excess ----
|
| 62 |
+
p = np.zeros((1, 9)); p[0, :3] = 0.2; p[0, 5] = 0.4
|
| 63 |
+
st = _cand_value_stats(logp_from_probs(p), np.array([BITS_123]))
|
| 64 |
+
check("leaky mass", st["mass"], 0.6)
|
| 65 |
+
if not (st["ce"] - st["floor"]) > 0.4:
|
| 66 |
+
fails.append("leaky excess")
|
| 67 |
+
print(f" leaky excess = {st['ce'] - st['floor']:.4f} (want > 0.4)")
|
| 68 |
+
|
| 69 |
+
# ---- uniform over all 9 digits: excess == log(9/|S|) ----
|
| 70 |
+
p = np.full((1, 9), 1.0 / 9.0)
|
| 71 |
+
st = _cand_value_stats(logp_from_probs(p), np.array([BITS_123]))
|
| 72 |
+
check("uniform-over-9 excess", st["ce"] - st["floor"], np.log(9) - np.log(3))
|
| 73 |
+
check("uniform-over-9 mass", st["mass"], 3.0 / 9.0)
|
| 74 |
+
check("uniform-over-9 spread", st["spread"], 1.0)
|
| 75 |
+
|
| 76 |
+
# ---- singleton set is excluded from spread but counted elsewhere ----
|
| 77 |
+
p = np.zeros((1, 9)); p[0, 4] = 1.0
|
| 78 |
+
st = _cand_value_stats(logp_from_probs(p), np.array([BITS_5]))
|
| 79 |
+
check("singleton excess", st["ce"] - st["floor"], 0.0)
|
| 80 |
+
check("singleton spread_count", st["spread_count"], 0)
|
| 81 |
+
check("singleton count", st["count"], 1)
|
| 82 |
+
|
| 83 |
+
# ---- the decomposition identity, on many random distributions ----
|
| 84 |
+
# excess = log(1/mass) + KL(Uniform(S) || p/mass)
|
| 85 |
+
# This is the whole claim: one number that is zero iff the support outside the
|
| 86 |
+
# candidate set has vanished AND the support inside it is uniform.
|
| 87 |
+
rng = np.random.default_rng(0)
|
| 88 |
+
worst = 0.0
|
| 89 |
+
for trial in range(2000):
|
| 90 |
+
k = int(rng.integers(2, 7))
|
| 91 |
+
cand = rng.choice(9, size=k, replace=False)
|
| 92 |
+
bits = int(sum(1 << int(d) for d in cand))
|
| 93 |
+
p = rng.dirichlet(np.full(9, float(rng.choice([0.15, 1.0, 5.0]))))
|
| 94 |
+
st = _cand_value_stats(logp_from_probs(p[None, :]), np.array([bits]))
|
| 95 |
+
excess = st["ce"] - st["floor"]
|
| 96 |
+
mass = st["mass"]
|
| 97 |
+
q = p[cand] / mass
|
| 98 |
+
kl = float(np.sum((1.0 / k) * np.log((1.0 / k) / q)))
|
| 99 |
+
worst = max(worst, abs(excess - (np.log(1.0 / mass) + kl)))
|
| 100 |
+
# both terms are non-negative, so excess is too
|
| 101 |
+
if excess < -1e-9 or kl < -1e-9:
|
| 102 |
+
fails.append("decomposition non-negativity")
|
| 103 |
+
# the code's own kl_multi must equal the independent one
|
| 104 |
+
if abs(st["kl_multi"] - kl) > 1e-8:
|
| 105 |
+
fails.append(f"kl_multi mismatch trial {trial}")
|
| 106 |
+
break
|
| 107 |
+
print(f"{'PASS' if worst < 1e-8 else 'FAIL'} decomposition identity over 2000 "
|
| 108 |
+
f"random cases: max |excess - (log(1/mass) + KL)| = {worst:.2e}")
|
| 109 |
+
if worst >= 1e-8:
|
| 110 |
+
fails.append("decomposition identity")
|
| 111 |
+
|
| 112 |
+
# ---- the two failure modes are separated by the split ----
|
| 113 |
+
S = np.array([0b000010111]) # candidates {1,2,3,5}, |S| = 4
|
| 114 |
+
# (a) perfectly uniform inside S, but only 50% of the mass is inside
|
| 115 |
+
p = np.zeros((1, 9)); p[0, [0, 1, 2, 4]] = 0.125; p[0, [6, 7]] = 0.25
|
| 116 |
+
st = _cand_value_stats(logp_from_probs(p), S)
|
| 117 |
+
check("leak-only: kl term is 0", st["kl_multi"], 0.0)
|
| 118 |
+
check("leak-only: excess == log(1/mass)", st["ce"] - st["floor"], np.log(2.0))
|
| 119 |
+
# (b) all the mass inside S, but skewed across the candidates
|
| 120 |
+
p = np.zeros((1, 9)); p[0, 0] = 0.7; p[0, 1] = 0.1; p[0, 2] = 0.1; p[0, 4] = 0.1
|
| 121 |
+
st = _cand_value_stats(logp_from_probs(p), S)
|
| 122 |
+
check("skew-only: mass is 1", st["mass"], 1.0)
|
| 123 |
+
check("skew-only: excess == kl", st["ce"] - st["floor"], st["kl_multi"])
|
| 124 |
+
print(f" skew-only kl = {st['kl_multi']:.4f} (want > 0), "
|
| 125 |
+
f"spread = {st['spread']:.4f}")
|
| 126 |
+
if not st["kl_multi"] > 0.2:
|
| 127 |
+
fails.append("skew-only kl > 0.2")
|
| 128 |
+
|
| 129 |
+
# ---- singleton cells are excluded from the *_multi aggregates ----
|
| 130 |
+
st = _cand_value_stats(logp_from_probs(np.full((1, 9), 1.0 / 9.0)),
|
| 131 |
+
np.array([0b000010000]))
|
| 132 |
+
check("singleton excluded from excess_multi", st["excess_multi"], 0.0)
|
| 133 |
+
check("singleton excluded from mass_multi", st["mass_multi"], 0.0)
|
| 134 |
+
check("singleton excluded from kl_multi", st["kl_multi"], 0.0)
|
| 135 |
+
|
| 136 |
+
# ---- loc_wave: order up to ties within a wave ----
|
| 137 |
+
truth = [(0, 0), (0, 1), (0, 2), (1, 0)]
|
| 138 |
+
waves = np.array([0, 0, 1, 1])
|
| 139 |
+
|
| 140 |
+
check("wave exact order", _wave_order_score(truth, truth, waves, 12), 4)
|
| 141 |
+
# swapping within a wave must cost nothing
|
| 142 |
+
check("wave swap inside wave",
|
| 143 |
+
_wave_order_score([(0, 1), (0, 0), (1, 0), (0, 2)], truth, waves, 12), 4)
|
| 144 |
+
# pulling a later wave forward costs exactly those steps
|
| 145 |
+
check("wave 1 pulled ahead of wave 0",
|
| 146 |
+
_wave_order_score([(0, 2), (0, 0), (0, 1), (1, 0)], truth, waves, 12), 3)
|
| 147 |
+
# duplicates and non-cells score nothing and consume nothing
|
| 148 |
+
check("wave duplicates",
|
| 149 |
+
_wave_order_score([(0, 0), (0, 0), (0, 0), (0, 0)], truth, waves, 12), 1)
|
| 150 |
+
check("wave invalid cells",
|
| 151 |
+
_wave_order_score([(7, 7), (8, 8), (0, 0), (0, 1)], truth, waves, 12), 2)
|
| 152 |
+
|
| 153 |
+
# ---- LCS: a reordering is not wiped out the way a positional match is ----
|
| 154 |
+
check("lcs identical", _lcs_len(truth, truth), 4)
|
| 155 |
+
check("lcs one swap",
|
| 156 |
+
_lcs_len([(0, 1), (0, 0), (0, 2), (1, 0)], truth), 3)
|
| 157 |
+
check("lcs reversed", _lcs_len(truth[::-1], truth), 1)
|
| 158 |
+
|
| 159 |
+
print()
|
| 160 |
+
if fails:
|
| 161 |
+
print(f"{len(fails)} FAILURES: {fails}")
|
| 162 |
+
sys.exit(1)
|
| 163 |
+
print("all metric checks passed")
|
code/wavecurriculum_run/train/data.py
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2024 The Google Research Authors.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""Data loading procedure for Othello and Sudoku game.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import itertools
|
| 20 |
+
import os
|
| 21 |
+
import pickle
|
| 22 |
+
|
| 23 |
+
import jax
|
| 24 |
+
import numpy as np
|
| 25 |
+
import tensorflow as tf
|
| 26 |
+
from tensorflow.compat.v1 import gfile
|
| 27 |
+
|
| 28 |
+
import pdb
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class CurriculumState:
|
| 32 |
+
"""Mutable curriculum stage shared between the train loop and the sampler.
|
| 33 |
+
|
| 34 |
+
stage t (1..max_stage) is a REASONING-DEPTH stage: t latent slots are
|
| 35 |
+
active and the first t wave snapshots are supervised. Every puzzle is
|
| 36 |
+
available from step 0 -- difficulty is not gated.
|
| 37 |
+
|
| 38 |
+
The difficulty tag is deliberately unused as a curriculum axis. It takes
|
| 39 |
+
only 6 values (3..8 = hardest solver-strategy digit needed by any cell), so
|
| 40 |
+
it cannot express a 12-step ladder, it is uncorrelated with puzzle size
|
| 41 |
+
(r=-0.003 vs empty-cell count), and it explains only ~19% of the variance
|
| 42 |
+
in solver round count. The ladder is over propagation depth instead.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def __init__(self, stage=1, max_stage=12):
|
| 46 |
+
self.stage = stage
|
| 47 |
+
self.max_stage = max_stage
|
| 48 |
+
|
| 49 |
+
def unlocked_levels(self):
|
| 50 |
+
"""All levels, always. Kept for logging/compat with per-level reports."""
|
| 51 |
+
return list(range(3, 9))
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def compute_puzzle_levels(strategy_codes, start_index):
|
| 55 |
+
"""Per-puzzle difficulty level = max strategy digit over solution cells.
|
| 56 |
+
|
| 57 |
+
strategy_codes: (N, 81) int64 chain codes (digits = strategy applications).
|
| 58 |
+
Clue cells have code 0. Level clipped to [3, 8] (rare all-lone-single
|
| 59 |
+
puzzles fold into level 3).
|
| 60 |
+
"""
|
| 61 |
+
n = len(strategy_codes)
|
| 62 |
+
levels = np.zeros(n, dtype=np.int32)
|
| 63 |
+
chunk = 200000
|
| 64 |
+
for lo in range(0, n, chunk):
|
| 65 |
+
x = strategy_codes[lo:lo + chunk].astype(np.int64).copy()
|
| 66 |
+
m = np.zeros_like(x)
|
| 67 |
+
while x.any():
|
| 68 |
+
np.maximum(m, x % 10, out=m)
|
| 69 |
+
x //= 10
|
| 70 |
+
levels[lo:lo + chunk] = m.max(axis=1).astype(np.int32)
|
| 71 |
+
return np.clip(levels, 3, 8)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def create_dataset(config, bs, train, curriculum=None):
|
| 75 |
+
"""Create Sudoku dataset according to the config.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
config: a config object containing the hyparameters for the dataset
|
| 79 |
+
creation.
|
| 80 |
+
bs: batch size
|
| 81 |
+
train: whether the dataset is for train or eval
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
a tf.data.Dataset object
|
| 85 |
+
"""
|
| 86 |
+
ds, output_types, output_shapes = None, None, None
|
| 87 |
+
ds = SudokuDataset(config, train=train, curriculum=curriculum)
|
| 88 |
+
# Each example is (sequence with latent slots, solution, start_index,
|
| 89 |
+
# difficulty level, per-slot candidate-set targets, round-bin). The
|
| 90 |
+
# round-bin is 0 unless the round-count data curriculum is enabled.
|
| 91 |
+
K = int(getattr(config, "num_latent_slots", 0))
|
| 92 |
+
output_types = (tf.int32, tf.int32, tf.int32, tf.int32, tf.int32, tf.int32)
|
| 93 |
+
output_shapes = (
|
| 94 |
+
tf.TensorShape([config.seq_len]),
|
| 95 |
+
tf.TensorShape([config.block_size]),
|
| 96 |
+
tf.TensorShape([1]),
|
| 97 |
+
tf.TensorShape([1]),
|
| 98 |
+
tf.TensorShape([K, config.block_size]),
|
| 99 |
+
tf.TensorShape([1]),
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# Create a tf.data.Dataset object from the generator.
|
| 103 |
+
tf_ds = tf.data.Dataset.from_generator(
|
| 104 |
+
generator=ds, output_types=output_types, output_shapes=output_shapes)
|
| 105 |
+
|
| 106 |
+
# Repeat the dataset indefinitely.
|
| 107 |
+
tf_ds = tf_ds.repeat()
|
| 108 |
+
# Shuffle the dataset with a buffer size of 8 * bs and a seed of 0.
|
| 109 |
+
tf_ds = tf_ds.shuffle(8 * config.minibatch_size, seed=0)
|
| 110 |
+
# Batch the dataset with a batch size of bs.
|
| 111 |
+
tf_ds = tf_ds.batch(bs)
|
| 112 |
+
return tf_ds
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def prepare_tf_data(xs):
|
| 117 |
+
"""Convert a input batch from tf Tensors to numpy arrays."""
|
| 118 |
+
def _prepare(x):
|
| 119 |
+
return x._numpy() # pylint: disable=protected-access
|
| 120 |
+
|
| 121 |
+
return jax.tree_map(_prepare, xs)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def create_iter(config, bs, train, curriculum=None):
|
| 125 |
+
tf_ds = create_dataset(config, bs, train=train, curriculum=curriculum)
|
| 126 |
+
it = map(prepare_tf_data, tf_ds)
|
| 127 |
+
return it
|
| 128 |
+
|
| 129 |
+
class SudokuDataset:
|
| 130 |
+
"""Sudoku dataset."""
|
| 131 |
+
def __init__(self, config, train=True, curriculum=None):
|
| 132 |
+
self.config = config
|
| 133 |
+
self.train = train
|
| 134 |
+
self.curriculum = curriculum
|
| 135 |
+
self.num_latent_slots = int(getattr(config, "num_latent_slots", 0))
|
| 136 |
+
self.latent_token_id = int(getattr(config, "latent_token_id", 10))
|
| 137 |
+
self.rng = np.random.RandomState(config.seed if hasattr(config, "seed") else 0)
|
| 138 |
+
self.preprocess_sudoku()
|
| 139 |
+
self._load_candidate_masks()
|
| 140 |
+
self._load_round_bins()
|
| 141 |
+
self._load_instances()
|
| 142 |
+
self._build_instance_epoch_list()
|
| 143 |
+
|
| 144 |
+
def _load_instances(self):
|
| 145 |
+
"""Load superposition instances: one concrete assignment per row.
|
| 146 |
+
|
| 147 |
+
Replaces the multi-hot candidate target with ordinary value tokens. For
|
| 148 |
+
a puzzle at stage s there are several assignments, each picking one digit
|
| 149 |
+
per cell from that cell's stage-s candidate set, so the candidate set is
|
| 150 |
+
recoverable across instances instead of being supervised as a set.
|
| 151 |
+
|
| 152 |
+
assignments: (M, 81) uint8, cell = r*9+c
|
| 153 |
+
starts/counts: (N, S) row range for each (puzzle, stage)
|
| 154 |
+
"""
|
| 155 |
+
self.instances = None
|
| 156 |
+
d = getattr(self.config, "instance_dir", None)
|
| 157 |
+
if not d:
|
| 158 |
+
return
|
| 159 |
+
split = "train" if self.train else "test"
|
| 160 |
+
self.instances = np.load(
|
| 161 |
+
os.path.join(d, f"{split}_assignments.npy"), mmap_mode="r")
|
| 162 |
+
self.inst_starts = np.load(os.path.join(d, f"{split}_starts.npy"))
|
| 163 |
+
self.inst_counts = np.load(os.path.join(d, f"{split}_counts.npy"))
|
| 164 |
+
self.inst_stages = int(self.inst_starts.shape[1])
|
| 165 |
+
print(f"[inst] loaded {split} instances {self.instances.shape} "
|
| 166 |
+
f"over {self.inst_starts.shape[0]} puzzles, "
|
| 167 |
+
f"{self.inst_stages} stages", flush=True)
|
| 168 |
+
|
| 169 |
+
def _build_instance_epoch_list(self):
|
| 170 |
+
"""Schedule for the pinned stage, so each puzzle is shown many times.
|
| 171 |
+
|
| 172 |
+
Default sampling draws a puzzle uniformly and then one of its
|
| 173 |
+
assignments at random, so over 8k steps at batch 64 a given puzzle is
|
| 174 |
+
seen at most once and most of its instances are never seen at all. The
|
| 175 |
+
superposition at a cell is only visible to the model as the spread of
|
| 176 |
+
digits it sees at that cell across repeated showings of the SAME puzzle,
|
| 177 |
+
so the schedule is built explicitly and walked in shuffled epochs.
|
| 178 |
+
|
| 179 |
+
Two sources of instances:
|
| 180 |
+
|
| 181 |
+
uniform (instance_uniform_draws=N)
|
| 182 |
+
Synthesize N instances per puzzle on the fly, each empty cell drawn
|
| 183 |
+
uniformly and independently from its candidate set. The per-cell
|
| 184 |
+
digit frequencies are then equal by construction, so the
|
| 185 |
+
cross-entropy optimum at that cell IS the uniform superposition.
|
| 186 |
+
|
| 187 |
+
pool (instance_epochs=E)
|
| 188 |
+
Walk the stored assignments E times each. Their per-cell frequencies
|
| 189 |
+
are whatever the coverage-driven generator produced (a |S|=2 cell is
|
| 190 |
+
typically 4:1), so CE converges to that skew, not to uniform.
|
| 191 |
+
"""
|
| 192 |
+
self.inst_pairs = None
|
| 193 |
+
self.inst_uniform = 0
|
| 194 |
+
self.inst_pairs_stage = None
|
| 195 |
+
draws = int(getattr(self.config, "instance_uniform_draws", 0))
|
| 196 |
+
epochs = int(getattr(self.config, "instance_epochs", 0))
|
| 197 |
+
if not self.train or (draws <= 0 and epochs <= 0):
|
| 198 |
+
return
|
| 199 |
+
self._build_pairs_for_stage(self.instance_stage())
|
| 200 |
+
|
| 201 |
+
def _build_pairs_for_stage(self, stage):
|
| 202 |
+
"""(Re)build the schedule for one stage.
|
| 203 |
+
|
| 204 |
+
Called again on promotion: each stage has its own instance pool, so the
|
| 205 |
+
pair list and the pass counter both restart when the stage advances.
|
| 206 |
+
"""
|
| 207 |
+
draws = int(getattr(self.config, "instance_uniform_draws", 0))
|
| 208 |
+
epochs = int(getattr(self.config, "instance_epochs", 0))
|
| 209 |
+
n_puz = int(getattr(self.config, "instance_puzzles", 0))
|
| 210 |
+
n_all = len(self.train_puzzles)
|
| 211 |
+
n_puz = n_all if n_puz <= 0 else min(n_puz, n_all)
|
| 212 |
+
|
| 213 |
+
if draws > 0:
|
| 214 |
+
if self.cand_masks is None:
|
| 215 |
+
raise ValueError("instance_uniform_draws needs the candidate "
|
| 216 |
+
"masks (SUDOKU_TRAIN_CAND)")
|
| 217 |
+
self.inst_uniform = draws
|
| 218 |
+
widths = self._stage_widths(stage, min(n_puz, 2000))
|
| 219 |
+
pids = np.repeat(np.arange(n_puz, dtype=np.int32), draws)
|
| 220 |
+
# -1 = synthesize this instance instead of reading a stored row.
|
| 221 |
+
self.inst_pairs = (pids, np.full(len(pids), -1, dtype=np.int64))
|
| 222 |
+
print(f"[inst-uniform] stage {stage + 1}: {n_puz} puzzles x "
|
| 223 |
+
f"{draws} uniform draws = {len(pids)} examples; mean |S| "
|
| 224 |
+
f"{widths.mean():.2f}, max {widths.max()}; each candidate of "
|
| 225 |
+
f"a cell is drawn ~{draws / widths.mean():.1f} times "
|
| 226 |
+
f"(>=5 needs {int(5 * widths.max())} draws for the widest "
|
| 227 |
+
f"cell)", flush=True)
|
| 228 |
+
self.inst_pairs_stage = stage
|
| 229 |
+
return
|
| 230 |
+
|
| 231 |
+
if self.instances is None:
|
| 232 |
+
raise ValueError("instance_epochs needs SUDOKU_INSTANCE_DIR")
|
| 233 |
+
counts = np.asarray(self.inst_counts[:n_puz, stage]).astype(np.int64)
|
| 234 |
+
starts = np.asarray(self.inst_starts[:n_puz, stage]).astype(np.int64)
|
| 235 |
+
total = int(counts.sum())
|
| 236 |
+
# Expand (puzzle -> its `count` consecutive assignment rows) without a
|
| 237 |
+
# Python loop: repeat the puzzle id, then add the within-puzzle offset.
|
| 238 |
+
puzzle_ids = np.repeat(np.arange(n_puz, dtype=np.int64), counts)
|
| 239 |
+
offsets = (np.arange(total, dtype=np.int64)
|
| 240 |
+
- np.repeat(np.cumsum(counts) - counts, counts))
|
| 241 |
+
rows = np.repeat(starts, counts) + offsets
|
| 242 |
+
self.inst_pairs = (puzzle_ids.astype(np.int32), rows.astype(np.int64))
|
| 243 |
+
self.inst_epochs = epochs
|
| 244 |
+
self.inst_pairs_stage = stage
|
| 245 |
+
print(f"[inst] stage {stage + 1}: {n_puz} puzzles, {total} "
|
| 246 |
+
f"(puzzle, instance) pairs, {counts.min()}-{counts.max()} "
|
| 247 |
+
f"instances per puzzle (mean {counts.mean():.2f}); one epoch = "
|
| 248 |
+
f"every pair seen {epochs}x = {epochs * total} examples",
|
| 249 |
+
flush=True)
|
| 250 |
+
|
| 251 |
+
def instance_stage(self):
|
| 252 |
+
"""Stage whose instances this example should target.
|
| 253 |
+
|
| 254 |
+
Bound to the curriculum stage so the target's ambiguity matches the
|
| 255 |
+
latent depth: stage t runs t latent slots and supervises the stage-(t-1)
|
| 256 |
+
assignments, ending at the unique solution when t == S.
|
| 257 |
+
"""
|
| 258 |
+
S = int(getattr(self, "inst_stages", 0)) or int(self.num_stages)
|
| 259 |
+
if self.curriculum is None:
|
| 260 |
+
return S - 1
|
| 261 |
+
t = int(np.clip(self.curriculum.stage, 1, S))
|
| 262 |
+
return t - 1
|
| 263 |
+
|
| 264 |
+
def _stage_widths(self, stage, n_sample):
|
| 265 |
+
"""|S| for every multi-candidate cell over the first n_sample puzzles."""
|
| 266 |
+
m = np.asarray(self.cand_masks[:n_sample, stage]).astype(np.int64)
|
| 267 |
+
bits = ((m[..., None] >> np.arange(9)) & 1).sum(-1)
|
| 268 |
+
return bits[bits >= 2]
|
| 269 |
+
|
| 270 |
+
def uniform_instance_values(self, idx, stage):
|
| 271 |
+
"""Synthesize one assignment: each cell drawn uniformly from its set.
|
| 272 |
+
|
| 273 |
+
Returns (81,) digits indexed by cell = r*9+c. Cells the mask leaves
|
| 274 |
+
empty (mask 0) return 0, and the caller keeps the base sequence's digit
|
| 275 |
+
there, so the clue block is untouched.
|
| 276 |
+
|
| 277 |
+
The uniform draw over set bits is done by giving every set bit an iid
|
| 278 |
+
random key and taking the argmax: the max is equally likely to land on
|
| 279 |
+
any set bit, which is exactly a uniform choice, and it vectorizes over
|
| 280 |
+
all 81 cells at once.
|
| 281 |
+
"""
|
| 282 |
+
m = np.asarray(self.cand_masks[idx, stage]).astype(np.int64) # (81,)
|
| 283 |
+
bits = ((m[:, None] >> np.arange(9)) & 1).astype(np.float64) # (81, 9)
|
| 284 |
+
keys = self.rng.random_sample((81, 9)) * bits
|
| 285 |
+
vals = (keys.argmax(1) + 1).astype(np.int8)
|
| 286 |
+
return np.where(m > 0, vals, 0)
|
| 287 |
+
|
| 288 |
+
def instance_values(self, idx, stage, inst_row=None):
|
| 289 |
+
"""One assignment for (puzzle idx, stage).
|
| 290 |
+
|
| 291 |
+
inst_row pins an exact assignment row (epoch mode, so every instance is
|
| 292 |
+
visited a fixed number of times), -1 synthesizes a fresh uniform draw,
|
| 293 |
+
and None samples one of the stored rows at random.
|
| 294 |
+
"""
|
| 295 |
+
if inst_row is not None and int(inst_row) < 0:
|
| 296 |
+
return self.uniform_instance_values(idx, stage)
|
| 297 |
+
if inst_row is not None:
|
| 298 |
+
return np.asarray(self.instances[int(inst_row)])
|
| 299 |
+
n = int(self.inst_counts[idx, stage])
|
| 300 |
+
if n <= 0:
|
| 301 |
+
return None
|
| 302 |
+
row = int(self.inst_starts[idx, stage]) + self.rng.randint(n)
|
| 303 |
+
return np.asarray(self.instances[row])
|
| 304 |
+
|
| 305 |
+
def apply_instance(self, seq, idx, stage, inst_row=None):
|
| 306 |
+
"""Rewrite the value token of every triple to this instance's digit.
|
| 307 |
+
|
| 308 |
+
The (row, col) order is untouched, so the clue block and the solver-order
|
| 309 |
+
output sequence are exactly as before; only the values change.
|
| 310 |
+
"""
|
| 311 |
+
vals = self.instance_values(idx, stage, inst_row=inst_row)
|
| 312 |
+
if vals is None:
|
| 313 |
+
return seq
|
| 314 |
+
seq = seq.copy()
|
| 315 |
+
cells = seq[0::3].astype(np.int64) * 9 + seq[1::3].astype(np.int64)
|
| 316 |
+
new = vals[cells].astype(seq.dtype)
|
| 317 |
+
# 0 means "this cell has no candidate mask"; keep the base digit there
|
| 318 |
+
# so the clue block survives untouched. Stored assignments carry the
|
| 319 |
+
# clue digit itself, so they overwrite with the same value either way.
|
| 320 |
+
seq[2::3] = np.where(new > 0, new, seq[2::3])
|
| 321 |
+
return seq
|
| 322 |
+
|
| 323 |
+
def _load_round_bins(self):
|
| 324 |
+
"""Load per-puzzle solver round counts and bin them into max_stage
|
| 325 |
+
equal-count bins, for the round-count DATA curriculum.
|
| 326 |
+
|
| 327 |
+
The round count (waves needed to reach the unique solution, 5..38) is
|
| 328 |
+
the same propagation-depth axis the latent curriculum supervises, but
|
| 329 |
+
used to order the *puzzles* instead of the supervision. Bin edges are
|
| 330 |
+
always computed on the train split and reused for eval so a bin index
|
| 331 |
+
means the same thing in both.
|
| 332 |
+
"""
|
| 333 |
+
self.round_bins = None
|
| 334 |
+
self.num_bins = int(getattr(self.config, "curriculum_max_stage", 12))
|
| 335 |
+
if str(getattr(self.config, "data_curriculum", "none")) != "rounds":
|
| 336 |
+
return
|
| 337 |
+
tr_path = getattr(self.config, "train_meta_path", None)
|
| 338 |
+
path = tr_path if self.train else getattr(
|
| 339 |
+
self.config, "test_meta_path", None)
|
| 340 |
+
if not (path and tr_path):
|
| 341 |
+
raise ValueError(
|
| 342 |
+
"data_curriculum='rounds' needs SUDOKU_TRAIN_META and "
|
| 343 |
+
"SUDOKU_TEST_META (the *_meta.npy written by "
|
| 344 |
+
"staged_candidate_gen.py; column 2 is num_rounds)")
|
| 345 |
+
rounds = np.load(path, mmap_mode="r")[:, 2].astype(np.int32)
|
| 346 |
+
# Cut points from the TRAIN split, so a bin index means the same thing
|
| 347 |
+
# in eval. Round counts are integers with a peaked distribution (mean
|
| 348 |
+
# 22, sd 4), so raw quantiles collide -- the 12-bin quantiles repeat 20
|
| 349 |
+
# twice on the full corpus, which would leave a bin permanently empty
|
| 350 |
+
# and strand its stage with no frontier to measure. Force the cuts
|
| 351 |
+
# strictly increasing so every bin is reachable.
|
| 352 |
+
train_rounds = np.load(tr_path, mmap_mode="r")[:, 2].astype(np.int32)
|
| 353 |
+
edges = np.quantile(train_rounds, np.linspace(0, 1, self.num_bins + 1))
|
| 354 |
+
cuts = np.round(edges[1:-1]).astype(np.int64)
|
| 355 |
+
for i in range(1, len(cuts)):
|
| 356 |
+
if cuts[i] <= cuts[i - 1]:
|
| 357 |
+
cuts[i] = cuts[i - 1] + 1
|
| 358 |
+
# bin j (1-based) = stage that first unlocks the puzzle.
|
| 359 |
+
self.round_bins = np.clip(
|
| 360 |
+
np.searchsorted(cuts, rounds, side="right") + 1,
|
| 361 |
+
1, self.num_bins).astype(np.int32)
|
| 362 |
+
self.bin_index = {b: np.where(self.round_bins == b)[0]
|
| 363 |
+
for b in range(1, self.num_bins + 1)}
|
| 364 |
+
counts = {b: int(len(v)) for b, v in self.bin_index.items()}
|
| 365 |
+
print(f"[rounds] {'train' if self.train else 'eval'} bin counts:",
|
| 366 |
+
counts, flush=True)
|
| 367 |
+
print(f"[rounds] cuts: {cuts.tolist()} (rounds "
|
| 368 |
+
f"{int(rounds.min())}..{int(rounds.max())})", flush=True)
|
| 369 |
+
empty = [b for b, c in counts.items() if c == 0]
|
| 370 |
+
if empty and self.train:
|
| 371 |
+
raise ValueError(
|
| 372 |
+
f"round-bin curriculum has empty train bins {empty}; those "
|
| 373 |
+
f"stages would have no puzzles and no frontier signal")
|
| 374 |
+
|
| 375 |
+
def _load_candidate_masks(self):
|
| 376 |
+
"""Load staged candidate-set masks (N, S, 81) uint16, aligned by puzzle
|
| 377 |
+
index with the loaded .npy. Row i here == puzzle i in the base file."""
|
| 378 |
+
if self.train:
|
| 379 |
+
path = getattr(self.config, "train_cand_masks_path", None)
|
| 380 |
+
else:
|
| 381 |
+
path = getattr(self.config, "test_cand_masks_path", None)
|
| 382 |
+
self.cand_masks = None
|
| 383 |
+
self.num_stages = 0
|
| 384 |
+
if path:
|
| 385 |
+
self.cand_masks = np.load(path, mmap_mode="r")
|
| 386 |
+
self.num_stages = int(self.cand_masks.shape[1])
|
| 387 |
+
print(f"[cand] loaded {path} shape {self.cand_masks.shape}", flush=True)
|
| 388 |
+
|
| 389 |
+
def slot_budget(self, level):
|
| 390 |
+
"""Number of latent slots this example activates (see cand_slot_mode).
|
| 391 |
+
|
| 392 |
+
"depth" mode must agree with the recurrence depth used by the train
|
| 393 |
+
step, since build_latent_state only ever writes slots [0, num_passes):
|
| 394 |
+
supervising a slot the recurrence never filled would train the head off
|
| 395 |
+
an all-zero latent.
|
| 396 |
+
"""
|
| 397 |
+
K = self.num_latent_slots
|
| 398 |
+
if getattr(self.config, "cand_slot_mode", "level") == "depth":
|
| 399 |
+
stage = self.curriculum.stage if self.curriculum is not None \
|
| 400 |
+
else getattr(self.config, "curriculum_max_stage", 6)
|
| 401 |
+
pps = int(getattr(self.config, "passes_per_stage", 1))
|
| 402 |
+
return int(np.clip(pps * stage, 1, K))
|
| 403 |
+
return int(np.clip(level - 2, 1, K))
|
| 404 |
+
|
| 405 |
+
def _slot_stage_targets(self, idx, level, clue_cells=None):
|
| 406 |
+
"""Return (K, 81) int32 candidate bitmasks, one per latent slot.
|
| 407 |
+
|
| 408 |
+
The S stored stages are mapped onto the example's k active slots; see
|
| 409 |
+
cand_slot_mode for the two mappings ("level" re-paces the whole shrink
|
| 410 |
+
sequence into k slots, "depth" assigns slot j to stage j). Inactive slots
|
| 411 |
+
(j>=k) default to the final stage; they are masked out of the loss.
|
| 412 |
+
|
| 413 |
+
Although the array is laid out over all 81 cell positions (for a fixed
|
| 414 |
+
batch shape), the supervised targets are only the *empty* cells: clue
|
| 415 |
+
cells are zeroed out here as a sentinel (a genuine empty cell always has
|
| 416 |
+
>=1 candidate at every stage), and the loss ignores zero rows. So the
|
| 417 |
+
effective target per puzzle is (#empty cells) x 9, in solver order."""
|
| 418 |
+
K = self.num_latent_slots
|
| 419 |
+
if self.cand_masks is None or K == 0:
|
| 420 |
+
return np.zeros((K, 81), dtype=np.int32)
|
| 421 |
+
S = self.num_stages
|
| 422 |
+
k = self.slot_budget(level)
|
| 423 |
+
depth_mode = getattr(self.config, "cand_slot_mode", "level") == "depth"
|
| 424 |
+
stages = self.cand_masks[idx].astype(np.int32) # (S, 81)
|
| 425 |
+
out = np.zeros((K, 81), dtype=np.int32)
|
| 426 |
+
for j in range(K):
|
| 427 |
+
if j >= k:
|
| 428 |
+
# Inactive slot: masked out of the loss, value is irrelevant.
|
| 429 |
+
s = S - 1
|
| 430 |
+
elif depth_mode:
|
| 431 |
+
# Identity: slot j holds propagation block j, so growing the
|
| 432 |
+
# recurrence depth extends the chain instead of re-pacing it.
|
| 433 |
+
# The solution is only reached at full depth, which is what
|
| 434 |
+
# makes this a curriculum over reasoning depth.
|
| 435 |
+
s = min(j, S - 1)
|
| 436 |
+
else:
|
| 437 |
+
# Active slots span the full shrink sequence: slot 0 -> stage 0
|
| 438 |
+
# (widest candidate set, genuinely multi-valued), last active
|
| 439 |
+
# slot -> final stage (solution). For k==1 the single slot maps
|
| 440 |
+
# to the WIDEST set (stage 0), not the solution, so even level-3
|
| 441 |
+
# puzzles give the candidate head a real multi-candidate target
|
| 442 |
+
# (the LM head still produces the unique answer).
|
| 443 |
+
s = int(round(j * (S - 1) / max(k - 1, 1)))
|
| 444 |
+
out[j] = stages[s]
|
| 445 |
+
# Sentinel-zero the clue cells so only the empty cells are supervised.
|
| 446 |
+
if clue_cells is not None and len(clue_cells) > 0:
|
| 447 |
+
out[:, clue_cells] = 0
|
| 448 |
+
return out
|
| 449 |
+
|
| 450 |
+
def _build_level_index(self, levels):
|
| 451 |
+
"""Map difficulty level -> array of puzzle indices."""
|
| 452 |
+
return {lvl: np.where(levels == lvl)[0] for lvl in range(3, 9)}
|
| 453 |
+
|
| 454 |
+
def insert_latent_slots(self, seq, start_index):
|
| 455 |
+
"""Insert K latent placeholder tokens between clues and solution.
|
| 456 |
+
|
| 457 |
+
seq: (243,) triple sequence. Returns (243 + K,) sequence:
|
| 458 |
+
[clues (3*si)] [K placeholders] [solution triples].
|
| 459 |
+
"""
|
| 460 |
+
k = self.num_latent_slots
|
| 461 |
+
if k == 0:
|
| 462 |
+
return seq
|
| 463 |
+
si3 = 3 * int(start_index)
|
| 464 |
+
return np.concatenate([
|
| 465 |
+
seq[:si3],
|
| 466 |
+
np.full(k, self.latent_token_id, dtype=seq.dtype),
|
| 467 |
+
seq[si3:],
|
| 468 |
+
])
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
def convert_to_fixed_or_random_order(self, inputs, start_index):
|
| 472 |
+
"""Convert the sequence of moves to either a fixed or random order.
|
| 473 |
+
|
| 474 |
+
Args:
|
| 475 |
+
inputs: a numpy array of shape (num_puzzles, seq_len) containing the
|
| 476 |
+
sequence of moves for each puzzle
|
| 477 |
+
start_index: a numpy array of shape (num_puzzles, 1) containing the starting
|
| 478 |
+
index for each puzzle
|
| 479 |
+
|
| 480 |
+
Returns:
|
| 481 |
+
transformed_input: a numpy array of shape (num_puzzles, seq_len) containing the
|
| 482 |
+
sequence of moves for each puzzle in either a fixed or random order
|
| 483 |
+
"""
|
| 484 |
+
transformed_input = np.zeros_like(inputs)
|
| 485 |
+
|
| 486 |
+
for i in range(len(inputs)):
|
| 487 |
+
cur_seq = inputs[i]
|
| 488 |
+
cur_start_index = start_index[i, 0]
|
| 489 |
+
|
| 490 |
+
# Split the sequence into input and output prompts
|
| 491 |
+
inp_prompt = cur_seq[ :(3 * cur_start_index) ].reshape(-1, 3)
|
| 492 |
+
out_prompt = cur_seq[ (3 * cur_start_index): ].reshape(-1, 3)
|
| 493 |
+
|
| 494 |
+
# Sort the input prompts in a fixed order
|
| 495 |
+
if self.config.seq_order == "fixed":
|
| 496 |
+
transformed_input[i, :(3 * cur_start_index) ] = inp_prompt[ np.lexsort( inp_prompt[:, ::-1].T ) ].flatten()
|
| 497 |
+
# Randomly shuffle the input prompts
|
| 498 |
+
elif self.config.seq_order == "random":
|
| 499 |
+
transformed_input[i, :(3 * cur_start_index) ] = np.random.permutation(inp_prompt).flatten()
|
| 500 |
+
|
| 501 |
+
# Sort the output prompts in a fixed order
|
| 502 |
+
if self.config.seq_order == "fixed":
|
| 503 |
+
transformed_input[i, (3 * cur_start_index): ] = out_prompt[ np.lexsort( out_prompt[:, ::-1].T ) ].flatten()
|
| 504 |
+
# Randomly shuffle the output prompts
|
| 505 |
+
elif self.config.seq_order == "random":
|
| 506 |
+
transformed_input[i, (3 * cur_start_index): ] = np.random.permutation(out_prompt).flatten()
|
| 507 |
+
|
| 508 |
+
return transformed_input
|
| 509 |
+
|
| 510 |
+
def get_puzzles_start_index(self, path):
|
| 511 |
+
"""Get the puzzles, start index, inputs and difficulty levels.
|
| 512 |
+
|
| 513 |
+
Returns:
|
| 514 |
+
inputs: (num_puzzles, 243) move sequences (strategy column removed)
|
| 515 |
+
puzzles: (num_puzzles, 81) solutions
|
| 516 |
+
start_index: (num_puzzles, 1) number of clue cells
|
| 517 |
+
levels: (num_puzzles,) puzzle difficulty level in [3, 8]
|
| 518 |
+
(= hardest solver-strategy digit needed by any cell)
|
| 519 |
+
"""
|
| 520 |
+
with gfile.Open(path, "rb") as f:
|
| 521 |
+
inputs_with_start_index = np.load(f)
|
| 522 |
+
start_index = inputs_with_start_index[:, 0] # Get the start index
|
| 523 |
+
|
| 524 |
+
rest = inputs_with_start_index[:, 1:]
|
| 525 |
+
# Strategy chain codes (4th entry of each cell quadruple); keep them to
|
| 526 |
+
# derive the curriculum difficulty level, then remove from the inputs.
|
| 527 |
+
strategy_codes = rest.reshape(len(rest), 81, 4)[:, :, 3]
|
| 528 |
+
levels = compute_puzzle_levels(strategy_codes, start_index)
|
| 529 |
+
inputs = np.delete( rest, np.arange(81) * 4 + 3, axis=1)
|
| 530 |
+
|
| 531 |
+
puzzles = np.zeros((len(inputs), 81), dtype=np.int8) # Initialize puzzles
|
| 532 |
+
for j in range(81):
|
| 533 |
+
cell_id = inputs[:, 3 * j] * 9 + inputs[:, 3 * j + 1] # Get the cell id
|
| 534 |
+
puzzles[np.arange(len(inputs)), cell_id] = inputs[:, 3 * j + 2] # Set the puzzle
|
| 535 |
+
|
| 536 |
+
return inputs, puzzles, start_index.reshape(-1, 1), levels
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
def preprocess_sudoku(self):
|
| 540 |
+
"""Preprocess the sudoku for train and test datasets.
|
| 541 |
+
|
| 542 |
+
Depending on the `train` flag, this method loads and processes the
|
| 543 |
+
sudoku puzzles and their start indices from the appropriate paths, and
|
| 544 |
+
optionally converts them to a fixed or random order based on the
|
| 545 |
+
configuration.
|
| 546 |
+
"""
|
| 547 |
+
if self.train is True:
|
| 548 |
+
# Load train puzzles, inputs, and start indices
|
| 549 |
+
(self.train_inputs, self.train_puzzles, self.train_start_index,
|
| 550 |
+
self.train_levels) = (
|
| 551 |
+
self.get_puzzles_start_index(self.config.train_puzzle_path)
|
| 552 |
+
)
|
| 553 |
+
# Convert train inputs to fixed or random order if specified
|
| 554 |
+
if self.config.seq_order in {"fixed", "random"}:
|
| 555 |
+
self.train_inputs = self.convert_to_fixed_or_random_order(self.train_inputs, self.train_start_index)
|
| 556 |
+
self.level_index = self._build_level_index(self.train_levels)
|
| 557 |
+
print("train level counts:",
|
| 558 |
+
{l: len(v) for l, v in self.level_index.items()}, flush=True)
|
| 559 |
+
|
| 560 |
+
elif self.train is False:
|
| 561 |
+
# Load evaluation puzzles, inputs, and start indices
|
| 562 |
+
(self.eval_inputs, self.eval_puzzles, self.eval_start_index,
|
| 563 |
+
self.eval_levels) = (
|
| 564 |
+
self.get_puzzles_start_index(self.config.test_puzzle_path)
|
| 565 |
+
)
|
| 566 |
+
# Convert evaluation inputs to fixed or random order if specified
|
| 567 |
+
if self.config.seq_order in {"fixed", "random"}:
|
| 568 |
+
self.eval_inputs = self.convert_to_fixed_or_random_order(self.eval_inputs, self.eval_start_index)
|
| 569 |
+
self.level_index = self._build_level_index(self.eval_levels)
|
| 570 |
+
|
| 571 |
+
def __len__(self):
|
| 572 |
+
if self.train is True:
|
| 573 |
+
return len(self.train_puzzles)
|
| 574 |
+
elif self.train is False:
|
| 575 |
+
return len(self.eval_puzzles)
|
| 576 |
+
|
| 577 |
+
def __getitem__(self, idx, inst_row=None):
|
| 578 |
+
"""Returns one example: (sequence with latent slots, solution,
|
| 579 |
+
start_index, difficulty level).
|
| 580 |
+
|
| 581 |
+
The base sequence is 243 tokens of (row, column, value) triples; K
|
| 582 |
+
latent placeholder tokens are inserted after the clue block, giving
|
| 583 |
+
243 + K tokens. start_index is the number of clue cells; level in
|
| 584 |
+
[3, 8] is the hardest solver strategy needed by any cell.
|
| 585 |
+
"""
|
| 586 |
+
if self.train is True:
|
| 587 |
+
inputs, puzzles = self.train_inputs, self.train_puzzles
|
| 588 |
+
start_index, levels = self.train_start_index, self.train_levels
|
| 589 |
+
else:
|
| 590 |
+
inputs, puzzles = self.eval_inputs, self.eval_puzzles
|
| 591 |
+
start_index, levels = self.eval_start_index, self.eval_levels
|
| 592 |
+
|
| 593 |
+
base = inputs[idx, :]
|
| 594 |
+
if (self.instances is not None or getattr(self, "inst_uniform", 0)) \
|
| 595 |
+
and self.train:
|
| 596 |
+
# Same input prompt, different output prompt: the clue triples are
|
| 597 |
+
# untouched (their instance digit is the clue) while the empty cells
|
| 598 |
+
# take one draw from the stage's candidate sets. Averaged over the
|
| 599 |
+
# instances the target IS the candidate set, so the superposition is
|
| 600 |
+
# learned from ordinary next-token CE instead of a set head. Eval
|
| 601 |
+
# keeps the unique solution: the sequence it scores is generated, and
|
| 602 |
+
# `puzzles` must stay the ground truth the accuracy is measured on.
|
| 603 |
+
base = self.apply_instance(base, idx, self.instance_stage(),
|
| 604 |
+
inst_row=inst_row)
|
| 605 |
+
seq = self.insert_latent_slots(base, start_index[idx, 0])
|
| 606 |
+
# Clue cells = the first `start_index` (r,c,v) triples; their cell ids
|
| 607 |
+
# are excluded from candidate supervision (only empty cells are scored).
|
| 608 |
+
si = int(start_index[idx, 0])
|
| 609 |
+
clue_triples = inputs[idx, :3 * si].reshape(-1, 3)
|
| 610 |
+
clue_cells = (clue_triples[:, 0] * 9 + clue_triples[:, 1]).astype(np.int64)
|
| 611 |
+
cand_targets = self._slot_stage_targets(idx, int(levels[idx]), clue_cells)
|
| 612 |
+
rbin = (int(self.round_bins[idx]) if self.round_bins is not None else 0)
|
| 613 |
+
return (
|
| 614 |
+
seq,
|
| 615 |
+
puzzles[idx, :],
|
| 616 |
+
start_index[idx],
|
| 617 |
+
np.array([levels[idx]], dtype=np.int32),
|
| 618 |
+
cand_targets,
|
| 619 |
+
np.array([rbin], dtype=np.int32),
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
def _sample_level(self):
|
| 623 |
+
"""Uniform over levels that have puzzles. Used for level-balanced mode
|
| 624 |
+
and for eval, where per-level reporting needs every level represented."""
|
| 625 |
+
avail = [l for l in range(3, 9) if len(self.level_index[l]) > 0]
|
| 626 |
+
return avail[self.rng.randint(len(avail))]
|
| 627 |
+
|
| 628 |
+
def _sample_round_gated(self):
|
| 629 |
+
"""Uniform over puzzles whose round-bin is already unlocked (bin<=stage).
|
| 630 |
+
Reading the stage at yield time lets the pool grow on promotion."""
|
| 631 |
+
stage = self.curriculum.stage if self.curriculum is not None \
|
| 632 |
+
else self.num_bins
|
| 633 |
+
stage = int(np.clip(stage, 1, self.num_bins))
|
| 634 |
+
pool = np.concatenate([self.bin_index[b] for b in range(1, stage + 1)])
|
| 635 |
+
return int(pool[self.rng.randint(len(pool))])
|
| 636 |
+
|
| 637 |
+
def __call__(self):
|
| 638 |
+
# Infinite generator. Train draws puzzles uniformly from the whole
|
| 639 |
+
# corpus (natural difficulty mix, ~68% level 3): the difficulty tag
|
| 640 |
+
# selects nothing, since the curriculum axis is propagation depth.
|
| 641 |
+
# Eval stays level-balanced so per-level accuracy is measurable and
|
| 642 |
+
# comparable across runs.
|
| 643 |
+
#
|
| 644 |
+
# The exception is the round-count DATA curriculum, where the train pool
|
| 645 |
+
# is restricted to puzzles needing at most stage-many propagation waves.
|
| 646 |
+
# Eval is never gated: it must score the whole corpus at every stage.
|
| 647 |
+
round_gated = (self.train and self.round_bins is not None)
|
| 648 |
+
level_balanced = (not self.train) or bool(
|
| 649 |
+
int(getattr(self.config, "level_balanced_sampling", 0)))
|
| 650 |
+
n = len(self.train_puzzles) if self.train else len(self.eval_puzzles)
|
| 651 |
+
|
| 652 |
+
if getattr(self, "inst_pairs", None) is not None:
|
| 653 |
+
# Epoch mode: walk every (puzzle, instance) pair, reshuffled each
|
| 654 |
+
# epoch, so each instance of each puzzle is visited exactly once per
|
| 655 |
+
# epoch. Shuffling means a batch holds distinct puzzles, i.e. one
|
| 656 |
+
# instance of a given puzzle per batch rather than all of its
|
| 657 |
+
# instances side by side.
|
| 658 |
+
reps = max(int(getattr(self, "inst_epochs", 1)), 1)
|
| 659 |
+
npass = 0
|
| 660 |
+
while True:
|
| 661 |
+
stage = self.instance_stage()
|
| 662 |
+
if stage != self.inst_pairs_stage:
|
| 663 |
+
# Promotion: this stage has its own instance pool, so the
|
| 664 |
+
# pair list and the pass count both restart.
|
| 665 |
+
self._build_pairs_for_stage(stage)
|
| 666 |
+
npass = 0
|
| 667 |
+
puzzle_ids, rows = self.inst_pairs
|
| 668 |
+
total = len(rows)
|
| 669 |
+
order = np.arange(total)
|
| 670 |
+
self.rng.shuffle(order)
|
| 671 |
+
npass += 1
|
| 672 |
+
print(f"[inst] stage {stage + 1} pass {npass}: every "
|
| 673 |
+
f"(puzzle, instance) pair seen {npass}x of {reps} "
|
| 674 |
+
f"({total} pairs)", flush=True)
|
| 675 |
+
for i, t in enumerate(order):
|
| 676 |
+
# Promotion can land mid-pass; checking periodically keeps
|
| 677 |
+
# the targets on the current stage instead of finishing the
|
| 678 |
+
# old pool first. 256 examples is 4 batches.
|
| 679 |
+
if (i & 255) == 0 and self.instance_stage() != stage:
|
| 680 |
+
break
|
| 681 |
+
yield self.__getitem__(int(puzzle_ids[t]),
|
| 682 |
+
inst_row=int(rows[t]))
|
| 683 |
+
|
| 684 |
+
while True:
|
| 685 |
+
if round_gated:
|
| 686 |
+
idx = self._sample_round_gated()
|
| 687 |
+
elif level_balanced:
|
| 688 |
+
idx_arr = self.level_index[self._sample_level()]
|
| 689 |
+
idx = int(idx_arr[self.rng.randint(len(idx_arr))])
|
| 690 |
+
else:
|
| 691 |
+
idx = int(self.rng.randint(n))
|
| 692 |
+
yield self.__getitem__(idx)
|
code/wavecurriculum_run/train/evaluater.py
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluation related functions."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
from flax.training import common_utils
|
| 6 |
+
import jax
|
| 7 |
+
from jax import numpy as jnp
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
from train import model
|
| 11 |
+
|
| 12 |
+
import pdb
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _verbose_eval():
|
| 16 |
+
return os.environ.get("SUDOKU_VERBOSE_EVAL", "0") == "1"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _lcs_len(a, b):
|
| 20 |
+
"""Length of the longest common subsequence of two cell sequences.
|
| 21 |
+
|
| 22 |
+
Measures order agreement while tolerating insertions, so a single early
|
| 23 |
+
deviation costs one step instead of desyncing every later comparison the
|
| 24 |
+
way a positional match does.
|
| 25 |
+
"""
|
| 26 |
+
prev = [0] * (len(b) + 1)
|
| 27 |
+
for x in a:
|
| 28 |
+
cur = [0]
|
| 29 |
+
for k, y in enumerate(b):
|
| 30 |
+
cur.append(prev[k] + 1 if x == y else max(cur[k], prev[k + 1]))
|
| 31 |
+
prev = cur
|
| 32 |
+
return prev[-1]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _cand_value_stats(logp_digits, bits):
|
| 36 |
+
"""Value-head statistics against one stage's candidate sets.
|
| 37 |
+
|
| 38 |
+
The instance target at a cell is drawn uniformly from that cell's candidate
|
| 39 |
+
set S, so the CE the trainer minimizes is
|
| 40 |
+
CE = -(1/|S|) sum_{d in S} log p(d) >= log|S|,
|
| 41 |
+
with equality iff p == Uniform(S); collapsing onto one candidate sends it to
|
| 42 |
+
infinity. So CE - log|S| == KL(Uniform(S) || p) is zero exactly at true
|
| 43 |
+
superposition and grows from leakage outside S or collapse inside it.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
logp_digits: (n, 9) log-probabilities of digits 1..9 under the model.
|
| 47 |
+
bits: (n,) int bitmask; bit d-1 set iff digit d is a candidate.
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
Dict of sums over the n cells plus the counts they divide by. `spread`
|
| 51 |
+
(normalized within-set entropy, 1.0 = uniform over S) is only defined
|
| 52 |
+
for |S| >= 2, hence its own count.
|
| 53 |
+
"""
|
| 54 |
+
in_s = ((bits[:, None] >> np.arange(9)) & 1).astype(np.float64) # (n, 9)
|
| 55 |
+
size = in_s.sum(axis=1)
|
| 56 |
+
p_d = np.exp(logp_digits)
|
| 57 |
+
mass = (in_s * p_d).sum(axis=1)
|
| 58 |
+
ce = -(in_s * logp_digits).sum(axis=1) / size
|
| 59 |
+
# excess splits exactly into the two things we want driven to zero:
|
| 60 |
+
# excess = CE - log|S| = log(1/mass) + KL(Uniform(S) || p/mass)
|
| 61 |
+
# \_________/ \_________/
|
| 62 |
+
# leak outside non-uniformity inside
|
| 63 |
+
# Both terms are >= 0, so excess == 0 iff all mass is on S AND spread
|
| 64 |
+
# uniformly over it. Derivation: writing p(d) = mass * q(d) for d in S,
|
| 65 |
+
# CE = -log(mass) - (1/|S|) sum log q(d) = -log(mass) + log|S| + KL(U||q).
|
| 66 |
+
excess = ce - np.log(size)
|
| 67 |
+
out = {
|
| 68 |
+
"ce": float(ce.sum()),
|
| 69 |
+
"floor": float(np.log(size).sum()),
|
| 70 |
+
"mass": float(mass.sum()),
|
| 71 |
+
"count": int(size.size),
|
| 72 |
+
"spread": 0.0,
|
| 73 |
+
"spread_count": 0,
|
| 74 |
+
# Same quantities restricted to genuinely superposed cells (|S| >= 2).
|
| 75 |
+
# Singleton cells are already determined at this stage, so there is
|
| 76 |
+
# nothing to be uniform about; as the curriculum deepens they come to
|
| 77 |
+
# dominate the pooled average and dilute the superposition signal.
|
| 78 |
+
"excess_multi": 0.0,
|
| 79 |
+
"mass_multi": 0.0,
|
| 80 |
+
"kl_multi": 0.0,
|
| 81 |
+
}
|
| 82 |
+
multi = size >= 2
|
| 83 |
+
if multi.any():
|
| 84 |
+
m = np.maximum(mass[multi], 1e-12)
|
| 85 |
+
q = in_s[multi] * p_d[multi] / m[:, None]
|
| 86 |
+
h = -(q * np.log(np.maximum(q, 1e-12))).sum(axis=1)
|
| 87 |
+
out["spread"] = float((h / np.log(size[multi])).sum())
|
| 88 |
+
out["spread_count"] = int(multi.sum())
|
| 89 |
+
out["excess_multi"] = float(excess[multi].sum())
|
| 90 |
+
out["mass_multi"] = float(mass[multi].sum())
|
| 91 |
+
# KL(Uniform(S) || q): the non-uniformity term of the split above.
|
| 92 |
+
# Stricter than 1 - spread, because it weights every candidate equally
|
| 93 |
+
# and so blows up when the model drops one candidate to ~0.
|
| 94 |
+
out["kl_multi"] = float((excess[multi] + np.log(m)).sum())
|
| 95 |
+
return out
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _wave_order_score(emitted, truth, waves, max_wave):
|
| 99 |
+
"""How many emitted cells came from the earliest still-unfilled wave.
|
| 100 |
+
|
| 101 |
+
A cell's wave is the propagation depth that determines it. Cells sharing a
|
| 102 |
+
wave are order-interchangeable, so the solver order in the data is only one
|
| 103 |
+
valid linearization; crediting any cell from the frontier wave measures
|
| 104 |
+
"is the model respecting propagation depth" without penalizing an arbitrary
|
| 105 |
+
tie-break. Cells that repeat or are not real empty cells score nothing.
|
| 106 |
+
"""
|
| 107 |
+
left = dict(zip(truth, waves))
|
| 108 |
+
per_wave = np.bincount(np.asarray(waves, dtype=np.int64),
|
| 109 |
+
minlength=max_wave + 1)
|
| 110 |
+
min_w, ok = 0, 0
|
| 111 |
+
for cell in emitted:
|
| 112 |
+
while min_w <= max_wave and per_wave[min_w] == 0:
|
| 113 |
+
min_w += 1
|
| 114 |
+
if min_w > max_wave:
|
| 115 |
+
break
|
| 116 |
+
cw = left.pop(cell, None)
|
| 117 |
+
if cw is None:
|
| 118 |
+
continue
|
| 119 |
+
per_wave[cw] -= 1
|
| 120 |
+
ok += int(cw == min_w)
|
| 121 |
+
return ok
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def valid_solution(output_seq):
|
| 125 |
+
"""
|
| 126 |
+
This function checks if the puzzle is a valid solution by verifying if
|
| 127 |
+
each row, column and box has all the numbers from 1 to 9.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
output_seq: a numpy array of shape (243,) containing the sequence of
|
| 131 |
+
output numbers
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
int: 1 if correct solution, otherwise returns 0
|
| 135 |
+
"""
|
| 136 |
+
# rows[i, j] keeps track if ith row has received (j + 1) number
|
| 137 |
+
rows = np.zeros((9, 9))
|
| 138 |
+
# cols[i, j] keeps track if ith column has received (j + 1) number
|
| 139 |
+
cols = np.zeros((9, 9))
|
| 140 |
+
# boxes[i, j] keeps track if ith box has received (j + 1) number
|
| 141 |
+
boxes = np.zeros((9, 9))
|
| 142 |
+
|
| 143 |
+
for j in range(81):
|
| 144 |
+
# The row and column are in the range (0, 8) and puzzle values are in (1, 9)
|
| 145 |
+
if int(output_seq[3 * j]) >= 9:
|
| 146 |
+
return False
|
| 147 |
+
if int(output_seq[3 * j + 1]) >= 9:
|
| 148 |
+
return False
|
| 149 |
+
if int(output_seq[3 * j + 2]) > 9:
|
| 150 |
+
return False
|
| 151 |
+
|
| 152 |
+
row_num = int(output_seq[3 * j])
|
| 153 |
+
col_num = int(output_seq[3 * j + 1])
|
| 154 |
+
|
| 155 |
+
# Mark the number in the row, column and box
|
| 156 |
+
rows[row_num, int(output_seq[3 * j + 2] - 1)] += 1
|
| 157 |
+
cols[col_num, int(output_seq[3 * j + 2] - 1)] += 1
|
| 158 |
+
boxes[
|
| 159 |
+
int(3 * (row_num // 3) + (col_num // 3)), int(output_seq[3 * j + 2] - 1)
|
| 160 |
+
] += 1
|
| 161 |
+
|
| 162 |
+
if np.all(rows) and np.all(cols) and np.all(boxes):
|
| 163 |
+
return True
|
| 164 |
+
else:
|
| 165 |
+
return False
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def eval_step(state, batch, latent_vals, slot_pos, latent_active, config):
|
| 169 |
+
pred_logits, hidden, cand_logits = model.TransformerLMHeadModel(config).apply(
|
| 170 |
+
{"params": state.params}, batch, latent_values=latent_vals,
|
| 171 |
+
latent_positions=slot_pos, latent_active=latent_active,
|
| 172 |
+
)
|
| 173 |
+
return pred_logits, hidden, cand_logits
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def verify_sudoku_board(puzzle, row_num, col_num, num):
|
| 177 |
+
"""
|
| 178 |
+
Args:
|
| 179 |
+
puzzle (np.array): The correct Sudoku puzzle.
|
| 180 |
+
row_num (int): The row number (0-8).
|
| 181 |
+
col_num (int): The column number (0-8).
|
| 182 |
+
num (int): The number predicted at the specified row and column.
|
| 183 |
+
|
| 184 |
+
Raises:
|
| 185 |
+
AssertionError: If the row_num * 9 + col_num >= 81 or if the number at the specified row and column is not equal to the given number.
|
| 186 |
+
"""
|
| 187 |
+
if row_num * 9 + col_num >= 81:
|
| 188 |
+
assert False
|
| 189 |
+
|
| 190 |
+
assert puzzle[row_num * 9 + col_num] == num
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def get_eval_metrics(state, eval_data_iter, p_eval_step, config):
|
| 194 |
+
"""This function computes given evaluation metrics (e.g, accuracy) in eval metrics for each batch and appends the metric in the list of eval_metrics.
|
| 195 |
+
|
| 196 |
+
Args:
|
| 197 |
+
state: contains model parameters, optimizer, etc.
|
| 198 |
+
eval_data_iter: data iterator for evaluation dataset
|
| 199 |
+
p_eval_step: pmap function for forward pass of model for evaluation
|
| 200 |
+
config: general experiment config file
|
| 201 |
+
|
| 202 |
+
Returns:
|
| 203 |
+
eval_metrics: contains list of evaluation metrics for each batch
|
| 204 |
+
"""
|
| 205 |
+
|
| 206 |
+
eval_metrics = {
|
| 207 |
+
"acc": [], # Unique-solution placement (parent leftover; not printed as val_acc)
|
| 208 |
+
"loc_acc": [], # Location acc: model picks the ground-truth next cell (r,c)
|
| 209 |
+
# Permutation-tolerant location diagnostics. The upstream code scored
|
| 210 |
+
# only "acc" -- the digit at the cell the model chose -- and never
|
| 211 |
+
# compared (r,c) to the target order, because the solver order is one
|
| 212 |
+
# arbitrary linearization of a partial order: cells that become
|
| 213 |
+
# determined in the same propagation wave are interchangeable. loc_acc
|
| 214 |
+
# above therefore reads ~0.04 even when the model emits a valid
|
| 215 |
+
# permutation of the right cells, since one early deviation desyncs the
|
| 216 |
+
# rest of the positional comparison. These three replace it.
|
| 217 |
+
"loc_coverage": [], # distinct emitted cells that are really empty / #empty
|
| 218 |
+
"loc_dup": [], # fraction of emitted cells that repeat an earlier one
|
| 219 |
+
"loc_lcs": [], # longest common subsequence with target order / #empty
|
| 220 |
+
"loc_wave": [], # emitted cell sits in the earliest unfilled wave
|
| 221 |
+
"val_given_loc_acc": [], # Correct digit AMONG steps where location matched
|
| 222 |
+
"cand_bit_acc": [], # Per-digit accuracy of predicted candidate masks
|
| 223 |
+
"cand_set_acc": [], # Exact candidate-SET match per empty cell (all 9 bits)
|
| 224 |
+
"cand_set_acc_changed": [], # ...restricted to cells that changed this stage
|
| 225 |
+
"acc_complete_puzzle": [] # Accuracy of predicting correct complete puzzle
|
| 226 |
+
}
|
| 227 |
+
# Per-difficulty-level cell accuracy (levels 3..8). Diagnostic only: the
|
| 228 |
+
# curriculum no longer keys on level.
|
| 229 |
+
level_ok = {lvl: 0 for lvl in range(3, 9)}
|
| 230 |
+
level_tot = {lvl: 0 for lvl in range(3, 9)}
|
| 231 |
+
|
| 232 |
+
K = int(config.num_latent_slots)
|
| 233 |
+
|
| 234 |
+
# Per-SLOT candidate-set accuracy, i.e. per reasoning depth. Slot j holds
|
| 235 |
+
# wave snapshot j, so slot_ok[j]/slot_tot[j] is "how well is propagation
|
| 236 |
+
# block j predicted". This is the signal the depth curriculum promotes and
|
| 237 |
+
# backtracks on, replacing the old per-level accuracy.
|
| 238 |
+
slot_ok = np.zeros(max(K, 1), dtype=np.int64)
|
| 239 |
+
slot_tot = np.zeros(max(K, 1), dtype=np.int64)
|
| 240 |
+
slot_ok_ch = np.zeros(max(K, 1), dtype=np.int64)
|
| 241 |
+
slot_tot_ch = np.zeros(max(K, 1), dtype=np.int64)
|
| 242 |
+
|
| 243 |
+
# Per-stage in-set rate: was the emitted digit a *member* of that stage's
|
| 244 |
+
# candidate set? This is the promotion signal for the instance arm, where
|
| 245 |
+
# the target is one sampled assignment rather than the unique solution, so
|
| 246 |
+
# the model is right to emit any candidate. The candidate masks are read
|
| 247 |
+
# here as a metric only; nothing supervises them.
|
| 248 |
+
inset_ok = np.zeros(max(K, 1), dtype=np.int64)
|
| 249 |
+
inset_tot = np.zeros(max(K, 1), dtype=np.int64)
|
| 250 |
+
|
| 251 |
+
# Per-stage value-distribution stats, measured TEACHER-FORCED so the cell is
|
| 252 |
+
# given and the value head is scored in isolation from the model's choice of
|
| 253 |
+
# location. For a cell whose stage-s candidate set is S, the instance target
|
| 254 |
+
# is drawn uniformly from S, so the CE the trainer minimizes is
|
| 255 |
+
# CE = -(1/|S|) sum_{d in S} log p(d) >= log|S|,
|
| 256 |
+
# with equality iff p == Uniform(S). Collapsing onto a single candidate
|
| 257 |
+
# sends CE to infinity. So (CE - log|S|) == KL(Uniform(S) || p) is a single
|
| 258 |
+
# number that is 0 exactly at true superposition and grows from either
|
| 259 |
+
# leakage outside S or mode collapse inside it.
|
| 260 |
+
vce_sum = np.zeros(max(K, 1), dtype=np.float64) # sum of CE vs Uniform(S)
|
| 261 |
+
vfloor_sum = np.zeros(max(K, 1), dtype=np.float64) # sum of log|S|
|
| 262 |
+
vmass_sum = np.zeros(max(K, 1), dtype=np.float64) # sum of prob mass on S
|
| 263 |
+
vspread_sum = np.zeros(max(K, 1), dtype=np.float64) # sum of H(p|S)/log|S|
|
| 264 |
+
vcnt = np.zeros(max(K, 1), dtype=np.int64)
|
| 265 |
+
vspread_cnt = np.zeros(max(K, 1), dtype=np.int64) # only |S| >= 2 cells
|
| 266 |
+
# Same, restricted to |S| >= 2 (the cells that carry real superposition).
|
| 267 |
+
vexc_m_sum = np.zeros(max(K, 1), dtype=np.float64)
|
| 268 |
+
vmass_m_sum = np.zeros(max(K, 1), dtype=np.float64)
|
| 269 |
+
vkl_m_sum = np.zeros(max(K, 1), dtype=np.float64)
|
| 270 |
+
|
| 271 |
+
# Per-round-bin cell accuracy, for the round-count DATA curriculum: bin b is
|
| 272 |
+
# unlocked at stage b, so bin_ok[b]/bin_tot[b] measures competence on the
|
| 273 |
+
# puzzles that stage b introduced. This is the promotion signal for the arm
|
| 274 |
+
# that has no latent slots and therefore no per-depth signal.
|
| 275 |
+
n_bins = int(getattr(config, "curriculum_max_stage", 12))
|
| 276 |
+
bin_ok = {b: 0 for b in range(1, n_bins + 1)}
|
| 277 |
+
bin_tot = {b: 0 for b in range(1, n_bins + 1)}
|
| 278 |
+
|
| 279 |
+
for eval_epoch in range(config.eval_epochs):
|
| 280 |
+
with jax.profiler.StepTraceAnnotation("eval", step_num=eval_epoch):
|
| 281 |
+
|
| 282 |
+
batch_tuple = next(eval_data_iter)
|
| 283 |
+
|
| 284 |
+
# Input seq is (batchsize, 3*81 + K): clue triples, K latent
|
| 285 |
+
# placeholder slots, then solution triples.
|
| 286 |
+
input_seq = np.array(batch_tuple[0])
|
| 287 |
+
|
| 288 |
+
# Puzzle solution is of the shape (batchsize, 81). Each pos in {0,.., 80}
|
| 289 |
+
# for each puzzle contains value at cell (pos//9+1, pos%9 + 1)
|
| 290 |
+
puzzle_sol = np.array(batch_tuple[1])
|
| 291 |
+
start_index = np.array(batch_tuple[2])
|
| 292 |
+
levels = np.array(batch_tuple[3]).reshape(-1)
|
| 293 |
+
rbins = (np.array(batch_tuple[5]).reshape(-1)
|
| 294 |
+
if len(batch_tuple) > 5 else np.zeros_like(levels))
|
| 295 |
+
total_pred, sucess_pred = 0, 0
|
| 296 |
+
# Location = did the model emit the ground-truth next (r,c) cell.
|
| 297 |
+
loc_tot, loc_ok, val_given_loc_ok = 0, 0, 0
|
| 298 |
+
|
| 299 |
+
bs = input_seq.shape[0]
|
| 300 |
+
bidx = np.arange(bs)
|
| 301 |
+
si3 = 3 * start_index.reshape(-1)
|
| 302 |
+
slot_pos = si3[:, None] + np.arange(K)[None, :]
|
| 303 |
+
if getattr(config, "cand_slot_mode", "level") == "depth":
|
| 304 |
+
# Eval always builds all K latents, so score all K slots.
|
| 305 |
+
k_budget = np.full_like(levels, K)
|
| 306 |
+
else:
|
| 307 |
+
k_budget = np.clip(levels - 2, 1, K)
|
| 308 |
+
active_full = np.arange(K)[None, :] < k_budget[:, None]
|
| 309 |
+
|
| 310 |
+
def run_model(seq_batch, latent_vals, act, want_cand=False):
|
| 311 |
+
sharded = common_utils.shard(
|
| 312 |
+
jax.tree_util.tree_map(np.asarray, seq_batch))
|
| 313 |
+
# Explicit reshape so a zero-width slot dim (K=0 baseline)
|
| 314 |
+
# shards without the ambiguous -1 inference of shard().
|
| 315 |
+
_nd = jax.local_device_count()
|
| 316 |
+
def _shard(x):
|
| 317 |
+
x = np.asarray(x)
|
| 318 |
+
return x.reshape((_nd, x.shape[0] // _nd) + x.shape[1:])
|
| 319 |
+
lv = _shard(latent_vals)
|
| 320 |
+
lp = _shard(slot_pos)
|
| 321 |
+
la = _shard(act)
|
| 322 |
+
logits, hidden, cand = p_eval_step(state, sharded, lv, lp, la)
|
| 323 |
+
logits = np.array(logits).reshape(bs, *np.array(logits).shape[2:])
|
| 324 |
+
hidden = np.array(hidden).reshape(bs, *np.array(hidden).shape[2:])
|
| 325 |
+
if want_cand:
|
| 326 |
+
cand = np.array(cand).reshape(bs, *np.array(cand).shape[2:])
|
| 327 |
+
return logits, hidden, cand
|
| 328 |
+
return logits, hidden
|
| 329 |
+
|
| 330 |
+
# ---- Build the continuous latent thoughts (K recurrence passes,
|
| 331 |
+
# difficulty-matched budget; causal masking means only the clue
|
| 332 |
+
# region influences them). ----
|
| 333 |
+
latent_vals = np.zeros((bs, K, config.emb_dim), dtype=np.float32)
|
| 334 |
+
build_seq = np.array(input_seq)
|
| 335 |
+
build_seq_masked = np.array(build_seq)
|
| 336 |
+
# Hide the solution region during latent build (safety; causality
|
| 337 |
+
# already prevents leakage into slot hiddens).
|
| 338 |
+
for j in range(bs):
|
| 339 |
+
build_seq_masked[j, si3[j] + K:] = 0
|
| 340 |
+
# Recurrent feedback: build each latent thought from the previous
|
| 341 |
+
# slot's hidden. Skipped when the model does not inject latents
|
| 342 |
+
# (no-recurrence control): slots stay as static placeholders, so
|
| 343 |
+
# latent_vals is left at zeros and never used.
|
| 344 |
+
recurrent = bool(int(getattr(config, "recurrent_latent", 1)))
|
| 345 |
+
if recurrent and K > 0:
|
| 346 |
+
for j in range(K):
|
| 347 |
+
act_j = active_full & (np.arange(K)[None, :] < j)
|
| 348 |
+
_, hidden = run_model(build_seq_masked, latent_vals, act_j)
|
| 349 |
+
src = si3 - 1 + j
|
| 350 |
+
latent_vals[:, j] = hidden[bidx, src]
|
| 351 |
+
|
| 352 |
+
# ---- Candidate-set prediction accuracy (the multi-value target) ----
|
| 353 |
+
# One forward pass with the fully-built latents; read the per-slot
|
| 354 |
+
# candidate head and compare to the staged bitmask targets, scored
|
| 355 |
+
# only over active slots and empty cells (clue cells were zeroed).
|
| 356 |
+
# Skipped entirely for the K=0 no-latent baseline (no candidate head).
|
| 357 |
+
pred_bits = tgt_bits = cand_targets = None
|
| 358 |
+
if K > 0:
|
| 359 |
+
cand_targets = np.array(batch_tuple[4]).astype(np.int64) # (bs, K, 81)
|
| 360 |
+
# The candidate head is off in the instance arm (aux weight 0), so
|
| 361 |
+
# skip its forward pass and set metrics; the masks above are still
|
| 362 |
+
# read for the in-set rate.
|
| 363 |
+
if K > 0 and float(getattr(config, "aux_cand_weight", 1.0)) > 0.0:
|
| 364 |
+
_, _, cand_logits = run_model(
|
| 365 |
+
build_seq_masked, latent_vals, active_full, want_cand=True) # (bs,K,81,9)
|
| 366 |
+
pred_bits = (np.array(cand_logits) > 0.0) # sigmoid>0.5
|
| 367 |
+
tgt_bits = ((cand_targets[..., None] >> np.arange(9)) & 1).astype(bool)
|
| 368 |
+
valid = (cand_targets > 0) & active_full[:, :, None] # (bs,K,81)
|
| 369 |
+
if valid.sum() > 0:
|
| 370 |
+
bit_match = (pred_bits == tgt_bits) # (bs,K,81,9)
|
| 371 |
+
eval_metrics["cand_bit_acc"].append(
|
| 372 |
+
float(bit_match[valid].mean()))
|
| 373 |
+
eval_metrics["cand_set_acc"].append(
|
| 374 |
+
float(bit_match.all(axis=3)[valid].mean()))
|
| 375 |
+
# Same score restricted to cells whose candidate set
|
| 376 |
+
# actually changed from the previous stage. The unrestricted
|
| 377 |
+
# metrics above are dominated by cells that are unchanged
|
| 378 |
+
# copies of slot j-1, so they stay high for a head that has
|
| 379 |
+
# learned nothing but "repeat the previous slot".
|
| 380 |
+
changed = np.concatenate(
|
| 381 |
+
[np.ones_like(cand_targets[:, :1], dtype=bool),
|
| 382 |
+
cand_targets[:, 1:] != cand_targets[:, :-1]], axis=1)
|
| 383 |
+
valid_ch = valid & changed
|
| 384 |
+
if valid_ch.sum() > 0:
|
| 385 |
+
eval_metrics["cand_set_acc_changed"].append(
|
| 386 |
+
float(bit_match.all(axis=3)[valid_ch].mean()))
|
| 387 |
+
# Accumulate the same score split by slot (= depth).
|
| 388 |
+
set_match = bit_match.all(axis=3) # (bs,K,81)
|
| 389 |
+
slot_ok += (set_match & valid).sum(axis=(0, 2))
|
| 390 |
+
slot_tot += valid.sum(axis=(0, 2))
|
| 391 |
+
slot_ok_ch += (set_match & valid_ch).sum(axis=(0, 2))
|
| 392 |
+
slot_tot_ch += valid_ch.sum(axis=(0, 2))
|
| 393 |
+
|
| 394 |
+
# ---- Teacher-forced value distribution vs the candidate set ----
|
| 395 |
+
# One forward pass on the ground-truth solver-order sequence. The
|
| 396 |
+
# prefix pins down which cell each value slot refers to, so this
|
| 397 |
+
# measures the value head alone: location cannot contaminate it,
|
| 398 |
+
# and no sampling is needed because the full softmax is available.
|
| 399 |
+
if K > 0 and cand_targets is not None:
|
| 400 |
+
tf_logits, _ = run_model(input_seq, latent_vals, active_full)
|
| 401 |
+
# log_softmax over the whole vocab, matching the training CE.
|
| 402 |
+
tf_shift = tf_logits - tf_logits.max(axis=-1, keepdims=True)
|
| 403 |
+
tf_logp = tf_shift - np.log(
|
| 404 |
+
np.exp(tf_shift).sum(axis=-1, keepdims=True))
|
| 405 |
+
for j in range(bs):
|
| 406 |
+
si = int(start_index[j].reshape(-1)[0])
|
| 407 |
+
n_out = 81 - si
|
| 408 |
+
if n_out <= 0:
|
| 409 |
+
continue
|
| 410 |
+
base = 3 * si + K
|
| 411 |
+
t = np.arange(n_out)
|
| 412 |
+
v_pos = base + 3 * t + 2 # value token positions
|
| 413 |
+
if v_pos[-1] >= config.seq_len:
|
| 414 |
+
keep = v_pos < config.seq_len
|
| 415 |
+
t, v_pos = t[keep], v_pos[keep]
|
| 416 |
+
if t.size == 0:
|
| 417 |
+
continue
|
| 418 |
+
# Token ids are the numbers themselves: rows/cols 0..8 and
|
| 419 |
+
# digits 1..9, so digit d lives at vocab index d.
|
| 420 |
+
cells = (input_seq[j, base + 3 * t] * 9
|
| 421 |
+
+ input_seq[j, base + 3 * t + 1]).astype(np.int64)
|
| 422 |
+
ok_cell = (cells >= 0) & (cells < 81)
|
| 423 |
+
# logits at index p-1 predict the token at position p.
|
| 424 |
+
logp_d = tf_logp[j, v_pos - 1, 1:10] # (n, 9)
|
| 425 |
+
for s in range(K):
|
| 426 |
+
bits = cand_targets[j, s, np.where(ok_cell, cells, 0)]
|
| 427 |
+
# bits <= 0 marks a clue cell, which is not supervised.
|
| 428 |
+
sel = ok_cell & (bits > 0)
|
| 429 |
+
if not sel.any():
|
| 430 |
+
continue
|
| 431 |
+
st = _cand_value_stats(logp_d[sel], bits[sel])
|
| 432 |
+
vce_sum[s] += st["ce"]
|
| 433 |
+
vfloor_sum[s] += st["floor"]
|
| 434 |
+
vmass_sum[s] += st["mass"]
|
| 435 |
+
vcnt[s] += st["count"]
|
| 436 |
+
vspread_sum[s] += st["spread"]
|
| 437 |
+
vspread_cnt[s] += st["spread_count"]
|
| 438 |
+
vexc_m_sum[s] += st["excess_multi"]
|
| 439 |
+
vmass_m_sum[s] += st["mass_multi"]
|
| 440 |
+
vkl_m_sum[s] += st["kl_multi"]
|
| 441 |
+
|
| 442 |
+
min_start_index = int(np.min(start_index))
|
| 443 |
+
cur_input_seq = input_seq[:, :(min_start_index*3)]
|
| 444 |
+
for i in range(min_start_index * 3, config.seq_len):
|
| 445 |
+
### In i^th iteration, i^th number in sequence will predict
|
| 446 |
+
padding = np.zeros((input_seq.shape[0],
|
| 447 |
+
config.seq_len - len(cur_input_seq[0])),
|
| 448 |
+
dtype=np.int32)
|
| 449 |
+
concat_batch = np.hstack((cur_input_seq, padding))
|
| 450 |
+
|
| 451 |
+
pred_logits, _ = run_model(concat_batch, latent_vals, active_full)
|
| 452 |
+
|
| 453 |
+
# Positions < 3*start_index + K are given (clues + latent
|
| 454 |
+
# slots); the model predicts from there on. K is a multiple
|
| 455 |
+
# of 3, so the triple phase of i is unchanged.
|
| 456 |
+
if i%3 == 2:
|
| 457 |
+
# Model predicts the value at the cell (cur_input_seq[j][i-2],
|
| 458 |
+
# cur_input_seq[j][i-1])
|
| 459 |
+
max_number = pred_logits[:, i-1, :].argmax(axis=-1).flatten()
|
| 460 |
+
mask_arr = np.array(i >= (3 * start_index + K)).squeeze()
|
| 461 |
+
|
| 462 |
+
next_number = max_number * mask_arr + (1 - mask_arr) * input_seq[:, i]
|
| 463 |
+
|
| 464 |
+
cur_input_seq = np.hstack(
|
| 465 |
+
(cur_input_seq, np.reshape(next_number, (-1, 1)))
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
# Iterate through all examples in batch and calculate successful
|
| 469 |
+
# predictions of numbers
|
| 470 |
+
for j in range(len(cur_input_seq)):
|
| 471 |
+
if not mask_arr[j]:
|
| 472 |
+
continue
|
| 473 |
+
|
| 474 |
+
total_pred += 1
|
| 475 |
+
level_tot[int(levels[j])] += 1
|
| 476 |
+
if int(rbins[j]) in bin_tot:
|
| 477 |
+
bin_tot[int(rbins[j])] += 1
|
| 478 |
+
|
| 479 |
+
# Location accuracy: did the model emit the ground-truth
|
| 480 |
+
# next cell (r,c) for this solver-order step?
|
| 481 |
+
loc_tot += 1
|
| 482 |
+
loc_match = (int(cur_input_seq[j][i-2]) == int(input_seq[j, i-2])
|
| 483 |
+
and int(cur_input_seq[j][i-1]) == int(input_seq[j, i-1]))
|
| 484 |
+
if loc_match:
|
| 485 |
+
loc_ok += 1
|
| 486 |
+
|
| 487 |
+
# In-set rate per stage, scored at the ground-truth cell
|
| 488 |
+
# so a wrong location cannot make a digit vacuously
|
| 489 |
+
# legal. cand_targets[j, s, cell] is stage s's bitmask
|
| 490 |
+
# under cand_slot_mode="depth" (slot s <-> stage s).
|
| 491 |
+
if cand_targets is not None and loc_match:
|
| 492 |
+
cell = (int(input_seq[j, i-2]) * 9
|
| 493 |
+
+ int(input_seq[j, i-1]))
|
| 494 |
+
v = int(cur_input_seq[j][i])
|
| 495 |
+
for s in range(K):
|
| 496 |
+
bits = int(cand_targets[j, s, cell])
|
| 497 |
+
if bits <= 0: # clue cell, not supervised
|
| 498 |
+
continue
|
| 499 |
+
inset_tot[s] += 1
|
| 500 |
+
if 1 <= v <= 9 and (bits >> (v - 1)) & 1:
|
| 501 |
+
inset_ok[s] += 1
|
| 502 |
+
|
| 503 |
+
try:
|
| 504 |
+
verify_sudoku_board(puzzle_sol[j], cur_input_seq[j][i-2],
|
| 505 |
+
cur_input_seq[j][i-1], cur_input_seq[j][i])
|
| 506 |
+
except AssertionError:
|
| 507 |
+
# Mistake
|
| 508 |
+
pass
|
| 509 |
+
else:
|
| 510 |
+
sucess_pred += 1
|
| 511 |
+
level_ok[int(levels[j])] += 1
|
| 512 |
+
if int(rbins[j]) in bin_ok:
|
| 513 |
+
bin_ok[int(rbins[j])] += 1
|
| 514 |
+
if loc_match:
|
| 515 |
+
val_given_loc_ok += 1
|
| 516 |
+
else:
|
| 517 |
+
# Model predicts either a row number or column number
|
| 518 |
+
max_pos = pred_logits[:, i-1, :].argmax(axis=-1).flatten()
|
| 519 |
+
mask = (i >= (3 * start_index + K)).squeeze()
|
| 520 |
+
next_pos = max_pos * mask + (1 - mask) * input_seq[:, i]
|
| 521 |
+
|
| 522 |
+
# pdb.set_trace()
|
| 523 |
+
cur_input_seq = np.hstack(
|
| 524 |
+
(cur_input_seq, np.reshape(next_pos, (-1, 1)))
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
eval_metrics["acc"].append(sucess_pred * 1.0/ total_pred)
|
| 528 |
+
eval_metrics["loc_acc"].append(loc_ok * 1.0 / max(loc_tot, 1))
|
| 529 |
+
eval_metrics["val_given_loc_acc"].append(
|
| 530 |
+
val_given_loc_ok * 1.0 / max(loc_ok, 1))
|
| 531 |
+
|
| 532 |
+
def strip_latent_slots(seq, si):
|
| 533 |
+
return np.concatenate([seq[:3*si], seq[3*si + K:]])
|
| 534 |
+
|
| 535 |
+
# ---- Permutation-tolerant location diagnostics ----
|
| 536 |
+
# A cell's wave = the first stage at which its candidate set becomes
|
| 537 |
+
# a singleton, i.e. the propagation depth that determines it. Cells
|
| 538 |
+
# sharing a wave are order-interchangeable, so "did you name a cell
|
| 539 |
+
# from the earliest wave that is still unfilled" is the meaningful
|
| 540 |
+
# ordering signal; the exact index within the wave is arbitrary.
|
| 541 |
+
cov_b, dup_b, lcs_b, wav_b = [], [], [], []
|
| 542 |
+
for j in range(bs):
|
| 543 |
+
si = int(start_index[j].reshape(-1)[0])
|
| 544 |
+
pred = strip_latent_slots(cur_input_seq[j], si)
|
| 545 |
+
true = strip_latent_slots(input_seq[j], si)
|
| 546 |
+
emitted = [(int(pred[3 * k]), int(pred[3 * k + 1]))
|
| 547 |
+
for k in range(si, 81)]
|
| 548 |
+
truth = [(int(true[3 * k]), int(true[3 * k + 1]))
|
| 549 |
+
for k in range(si, 81)]
|
| 550 |
+
if not truth or not emitted:
|
| 551 |
+
continue
|
| 552 |
+
n_true = len(truth)
|
| 553 |
+
cov_b.append(len(set(emitted) & set(truth)) / n_true)
|
| 554 |
+
dup_b.append(1.0 - len(set(emitted)) / len(emitted))
|
| 555 |
+
# LCS is quadratic, so sample a few examples per batch.
|
| 556 |
+
if j < 32:
|
| 557 |
+
lcs_b.append(_lcs_len(emitted, truth) / n_true)
|
| 558 |
+
|
| 559 |
+
if cand_targets is None or K == 0:
|
| 560 |
+
continue
|
| 561 |
+
cids = np.array([r * 9 + c for r, c in truth], dtype=np.int64)
|
| 562 |
+
bits = cand_targets[j][:, cids] # (K, n)
|
| 563 |
+
singleton = (bits > 0) & ((bits & (bits - 1)) == 0)
|
| 564 |
+
w = np.where(singleton.any(axis=0), singleton.argmax(axis=0), K)
|
| 565 |
+
wav_b.append(
|
| 566 |
+
_wave_order_score(emitted, truth, w, K) / n_true)
|
| 567 |
+
|
| 568 |
+
if cov_b:
|
| 569 |
+
eval_metrics["loc_coverage"].append(float(np.mean(cov_b)))
|
| 570 |
+
eval_metrics["loc_dup"].append(float(np.mean(dup_b)))
|
| 571 |
+
if lcs_b:
|
| 572 |
+
eval_metrics["loc_lcs"].append(float(np.mean(lcs_b)))
|
| 573 |
+
if wav_b:
|
| 574 |
+
eval_metrics["loc_wave"].append(float(np.mean(wav_b)))
|
| 575 |
+
|
| 576 |
+
# ---- Print one concrete example answer the model generated ----
|
| 577 |
+
if eval_epoch == 0:
|
| 578 |
+
j = 0
|
| 579 |
+
si = int(start_index[j, 0])
|
| 580 |
+
pred = strip_latent_slots(cur_input_seq[j], si)
|
| 581 |
+
shown, n_ok, n_tot = [], 0, 0
|
| 582 |
+
for k in range(si, 81):
|
| 583 |
+
r, c, v = int(pred[3*k]), int(pred[3*k+1]), int(pred[3*k+2])
|
| 584 |
+
tv = int(puzzle_sol[j][r*9+c]) if (0 <= r < 9 and 0 <= c < 9) else -1
|
| 585 |
+
ok = (0 <= r < 9 and 0 <= c < 9 and v == tv)
|
| 586 |
+
n_tot += 1; n_ok += int(ok)
|
| 587 |
+
if len(shown) < 12:
|
| 588 |
+
shown.append(f"({r},{c})->{v}[true {tv}]{'ok' if ok else 'X'}")
|
| 589 |
+
if _verbose_eval():
|
| 590 |
+
print(f"EXAMPLE (level={int(levels[j])}, k={int(k_budget[j])}): "
|
| 591 |
+
f"model emitted {n_tot} (r,c)->v triples for the empty cells "
|
| 592 |
+
f"(format: (row,col)->value[true T]); first 12:", flush=True)
|
| 593 |
+
print(" ", " ".join(shown), flush=True)
|
| 594 |
+
print(f"EXAMPLE cells-correct={n_ok}/{n_tot} "
|
| 595 |
+
f"valid_full_grid={valid_solution(pred)}", flush=True)
|
| 596 |
+
|
| 597 |
+
# Instance arm: emitted digit next to the deepest stage's
|
| 598 |
+
# candidate set, so it is visible whether the model is sitting
|
| 599 |
+
# inside the superposition or outside it.
|
| 600 |
+
if K > 0 and cand_targets is not None and pred_bits is None:
|
| 601 |
+
tgt = strip_latent_slots(input_seq[j], si)
|
| 602 |
+
shown = []
|
| 603 |
+
for t3 in range(si, min(si + 8, 81)):
|
| 604 |
+
r, c = int(tgt[3*t3]), int(tgt[3*t3+1])
|
| 605 |
+
bits = int(cand_targets[j, K-1, r*9+c])
|
| 606 |
+
cset = "".join(str(d+1) for d in range(9)
|
| 607 |
+
if (bits >> d) & 1)
|
| 608 |
+
shown.append(f"(r{r},c{c})->{int(pred[3*t3+2])} "
|
| 609 |
+
f"in{{{cset}}}")
|
| 610 |
+
if _verbose_eval():
|
| 611 |
+
print(f"EXAMPLE emitted vs stage-{K} candidate set:",
|
| 612 |
+
" ".join(shown), flush=True)
|
| 613 |
+
|
| 614 |
+
# ---- Candidate-set (multi-value) prediction for this puzzle ----
|
| 615 |
+
# Show, at the last active latent slot, predicted vs target
|
| 616 |
+
# candidate SETS for the first few empty cells. (No latent
|
| 617 |
+
# slots in the K=0 baseline, so nothing to show.)
|
| 618 |
+
if K > 0 and pred_bits is not None:
|
| 619 |
+
kj = int(k_budget[j]) - 1
|
| 620 |
+
def _digs(bitrow):
|
| 621 |
+
return "".join(str(d + 1) for d in range(9) if bitrow[d])
|
| 622 |
+
cand_shown = []
|
| 623 |
+
for cell in range(81):
|
| 624 |
+
if cand_targets[j, kj, cell] <= 0: # clue / not supervised
|
| 625 |
+
continue
|
| 626 |
+
r, c = cell // 9, cell % 9
|
| 627 |
+
pset = _digs(pred_bits[j, kj, cell])
|
| 628 |
+
tset = _digs(tgt_bits[j, kj, cell])
|
| 629 |
+
cand_shown.append(f"(r{r},c{c}) pred{{{pset}}} true{{{tset}}}")
|
| 630 |
+
if len(cand_shown) >= 8:
|
| 631 |
+
break
|
| 632 |
+
if _verbose_eval():
|
| 633 |
+
print(f"EXAMPLE candidate-set @slot{kj} (pred vs true):",
|
| 634 |
+
" ".join(cand_shown), flush=True)
|
| 635 |
+
|
| 636 |
+
correct_eval_sudoku_puzzle = 0
|
| 637 |
+
|
| 638 |
+
for i in range(len(cur_input_seq)):
|
| 639 |
+
|
| 640 |
+
# increase correct_eval_sudoku_puzzle when the model output solution
|
| 641 |
+
# for a given puzzle is correct
|
| 642 |
+
stripped = strip_latent_slots(cur_input_seq[i], int(start_index[i, 0]))
|
| 643 |
+
correct_eval_sudoku_puzzle += valid_solution(stripped)
|
| 644 |
+
|
| 645 |
+
eval_metrics["acc_complete_puzzle"].append(
|
| 646 |
+
correct_eval_sudoku_puzzle * 1.0 / len(cur_input_seq)
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
per_level = {lvl: (level_ok[lvl] / level_tot[lvl] if level_tot[lvl] else -1.0)
|
| 650 |
+
for lvl in range(3, 9)}
|
| 651 |
+
eval_metrics["per_level_acc"] = per_level
|
| 652 |
+
if _verbose_eval():
|
| 653 |
+
print("PER-LEVEL cell acc:",
|
| 654 |
+
{lvl: (f"{v:.3f}" if v >= 0 else "n/a") for lvl, v in per_level.items()},
|
| 655 |
+
flush=True)
|
| 656 |
+
|
| 657 |
+
# Per-depth candidate-set accuracy, keyed by stage (slot j -> stage j+1) so
|
| 658 |
+
# the curriculum controller can index it directly by stage number.
|
| 659 |
+
per_slot = {j + 1: (float(slot_ok[j] / slot_tot[j]) if slot_tot[j] else -1.0)
|
| 660 |
+
for j in range(K)}
|
| 661 |
+
per_slot_ch = {j + 1: (float(slot_ok_ch[j] / slot_tot_ch[j])
|
| 662 |
+
if slot_tot_ch[j] else -1.0) for j in range(K)}
|
| 663 |
+
eval_metrics["per_slot_acc"] = per_slot
|
| 664 |
+
eval_metrics["per_slot_acc_changed"] = per_slot_ch
|
| 665 |
+
|
| 666 |
+
# Keyed by stage (slot s -> stage s+1) to match per_slot_acc.
|
| 667 |
+
per_stage_inset = {s + 1: (float(inset_ok[s] / inset_tot[s])
|
| 668 |
+
if inset_tot[s] else -1.0) for s in range(K)}
|
| 669 |
+
eval_metrics["per_stage_inset_acc"] = per_stage_inset
|
| 670 |
+
|
| 671 |
+
# Per-stage value-distribution metrics, keyed by stage to match the above.
|
| 672 |
+
# val_excess = CE(uniform-over-candidates || model) - log|S| >= 0 is the
|
| 673 |
+
# superposition score: 0 means the model spreads exactly uniformly over the
|
| 674 |
+
# stage's candidate set, and it rises if probability leaks outside the set
|
| 675 |
+
# or collapses onto one member of it.
|
| 676 |
+
def _per_stage(num, den):
|
| 677 |
+
return {s + 1: (float(num[s] / den[s]) if den[s] else -1.0)
|
| 678 |
+
for s in range(K)}
|
| 679 |
+
|
| 680 |
+
per_stage_vce = _per_stage(vce_sum, vcnt)
|
| 681 |
+
per_stage_vfloor = _per_stage(vfloor_sum, vcnt)
|
| 682 |
+
per_stage_vexcess = {
|
| 683 |
+
s: (per_stage_vce[s] - per_stage_vfloor[s]
|
| 684 |
+
if per_stage_vce[s] >= 0 else -1.0) for s in per_stage_vce}
|
| 685 |
+
eval_metrics["per_stage_val_ce"] = per_stage_vce
|
| 686 |
+
eval_metrics["per_stage_val_floor"] = per_stage_vfloor
|
| 687 |
+
eval_metrics["per_stage_val_excess"] = per_stage_vexcess
|
| 688 |
+
eval_metrics["per_stage_val_mass"] = _per_stage(vmass_sum, vcnt)
|
| 689 |
+
eval_metrics["per_stage_val_spread"] = _per_stage(vspread_sum, vspread_cnt)
|
| 690 |
+
# Superposition metrics on |S| >= 2 cells only. excess_multi is the single
|
| 691 |
+
# number to drive to 0: it equals log(1/mass_multi) + kl_multi, so it falls
|
| 692 |
+
# only when leakage outside the set AND non-uniformity inside it both fall.
|
| 693 |
+
eval_metrics["per_stage_val_excess_multi"] = _per_stage(
|
| 694 |
+
vexc_m_sum, vspread_cnt)
|
| 695 |
+
eval_metrics["per_stage_val_mass_multi"] = _per_stage(
|
| 696 |
+
vmass_m_sum, vspread_cnt)
|
| 697 |
+
eval_metrics["per_stage_val_kl_multi"] = _per_stage(vkl_m_sum, vspread_cnt)
|
| 698 |
+
if _verbose_eval() and K > 0 and any(v >= 0 for v in per_stage_vce.values()):
|
| 699 |
+
print("PER-STAGE val_excess (0 = uniform over candidate set):",
|
| 700 |
+
{s: (f"{v:.3f}" if per_stage_vce[s] >= 0 else "n/a")
|
| 701 |
+
for s, v in per_stage_vexcess.items()}, flush=True)
|
| 702 |
+
|
| 703 |
+
if _verbose_eval() and K > 0 and any(v >= 0 for v in per_stage_inset.values()):
|
| 704 |
+
print("PER-STAGE in-set rate (emitted digit is a stage-s candidate):",
|
| 705 |
+
{s: (f"{v:.3f}" if v >= 0 else "n/a")
|
| 706 |
+
for s, v in per_stage_inset.items()}, flush=True)
|
| 707 |
+
|
| 708 |
+
per_bin = {b: (bin_ok[b] / bin_tot[b] if bin_tot[b] else -1.0)
|
| 709 |
+
for b in range(1, n_bins + 1)}
|
| 710 |
+
eval_metrics["per_bin_acc"] = per_bin
|
| 711 |
+
if _verbose_eval() and any(v >= 0 for v in per_bin.values()):
|
| 712 |
+
print("PER-ROUND-BIN cell acc:",
|
| 713 |
+
{b: (f"{v:.3f}" if v >= 0 else "n/a") for b, v in per_bin.items()},
|
| 714 |
+
flush=True)
|
| 715 |
+
if _verbose_eval() and K > 0:
|
| 716 |
+
print("PER-DEPTH cand-set acc:",
|
| 717 |
+
{s: (f"{v:.3f}" if v >= 0 else "n/a") for s, v in per_slot.items()},
|
| 718 |
+
flush=True)
|
| 719 |
+
print("PER-DEPTH cand-set acc (changed cells only):",
|
| 720 |
+
{s: (f"{v:.3f}" if v >= 0 else "n/a")
|
| 721 |
+
for s, v in per_slot_ch.items()}, flush=True)
|
| 722 |
+
|
| 723 |
+
return eval_metrics
|
code/wavecurriculum_run/train/main.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2024 The Google Research Authors.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""Main file for Sudoku GPT experiments."""
|
| 17 |
+
|
| 18 |
+
import os
|
| 19 |
+
import sys
|
| 20 |
+
|
| 21 |
+
from absl import app
|
| 22 |
+
from absl import flags
|
| 23 |
+
from absl import logging
|
| 24 |
+
|
| 25 |
+
from clu import platform
|
| 26 |
+
|
| 27 |
+
import jax
|
| 28 |
+
import tensorflow as tf
|
| 29 |
+
import wandb
|
| 30 |
+
|
| 31 |
+
import ml_collections
|
| 32 |
+
from ml_collections import config_flags
|
| 33 |
+
|
| 34 |
+
from train import train_and_evaluate
|
| 35 |
+
from train import train_backtrack
|
| 36 |
+
|
| 37 |
+
import pdb
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
sys.dont_write_bytecode = True
|
| 42 |
+
|
| 43 |
+
logging.set_verbosity(logging.INFO)
|
| 44 |
+
|
| 45 |
+
FLAGS = flags.FLAGS
|
| 46 |
+
|
| 47 |
+
_WORKDIR = flags.DEFINE_string(
|
| 48 |
+
'workdir',
|
| 49 |
+
None,
|
| 50 |
+
'Directory to store model data.')
|
| 51 |
+
_EXP_NAME = flags.DEFINE_string(
|
| 52 |
+
'exp_name',
|
| 53 |
+
None,
|
| 54 |
+
'Experiment name.')
|
| 55 |
+
_CKPT_LOC = flags.DEFINE_string(
|
| 56 |
+
'ckpt_loc',
|
| 57 |
+
None,
|
| 58 |
+
'Directory to restore model.')
|
| 59 |
+
|
| 60 |
+
config_flags.DEFINE_config_file(
|
| 61 |
+
'config',
|
| 62 |
+
None,
|
| 63 |
+
'File path to the training hyperparameter configuration.',
|
| 64 |
+
lock_config=True)
|
| 65 |
+
flags.mark_flags_as_required(['workdir', 'exp_name'])
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def get_config():
|
| 69 |
+
"""Get the default hyperparameter configuration.
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
A ConfigDict object.
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
# Common configuration for all experiments.
|
| 76 |
+
config = ml_collections.ConfigDict()
|
| 77 |
+
|
| 78 |
+
# Dataset choice
|
| 79 |
+
config.dataset = 'sudoku'
|
| 80 |
+
|
| 81 |
+
# Sequence order
|
| 82 |
+
config.seq_order = "solver-order" ## Choices = ["fixed", "solver-order", "random"]
|
| 83 |
+
|
| 84 |
+
# Training related parameters
|
| 85 |
+
config.max_steps = int(os.environ.get("SUDOKU_MAX_STEPS", 100000))
|
| 86 |
+
config.dtype = jax.numpy.bfloat16
|
| 87 |
+
config.minibatch_size = int(os.environ.get("SUDOKU_MINIBATCH", 64))
|
| 88 |
+
|
| 89 |
+
# Continuous latent thoughts (ATC / Coconut style) + curriculum.
|
| 90 |
+
# Set SUDOKU_LATENT_SLOTS=0 for the no-latent control baseline (plain
|
| 91 |
+
# transformer + difficulty curriculum, no recurrence, no candidate head).
|
| 92 |
+
config.num_latent_slots = int(os.environ.get("SUDOKU_LATENT_SLOTS", 6))
|
| 93 |
+
config.latent_token_id = 10 # placeholder id for inactive slots
|
| 94 |
+
# Recurrent latent feedback. 1 = full ATC/Coconut recurrence (slot k built
|
| 95 |
+
# from slot k-1's hidden). 0 = NO recurrence: the K stage slots + shared
|
| 96 |
+
# candidate head + curriculum + BCE supervision are all kept, but the latent
|
| 97 |
+
# hidden state is never fed back, so each stage is an independent parallel
|
| 98 |
+
# readout with no carried state (the "stagewise, no-recurrence" control).
|
| 99 |
+
config.recurrent_latent = int(os.environ.get("SUDOKU_RECURRENT", 1))
|
| 100 |
+
# Backtracking (stage-replay) training: when 1, use the train_backtrack loop
|
| 101 |
+
# which interleaves frontier-stage batches with replays of earlier stages at
|
| 102 |
+
# their MATCHED recurrence depth (num_passes=t for stage t) to mitigate
|
| 103 |
+
# forgetting. backtrack_prob = fraction of steps that are replay steps.
|
| 104 |
+
config.backtrack = int(os.environ.get("SUDOKU_BACKTRACK", 0))
|
| 105 |
+
# Replay strategy: "prob" = fixed-probability replay of a random earlier stage
|
| 106 |
+
# (backtrack_prob). "adaptive" = deficit-driven controller that only replays a
|
| 107 |
+
# stage once its held-out accuracy has regressed below its graduation value by
|
| 108 |
+
# backtrack_margin, repairs the most-deficient stage first with a FULL
|
| 109 |
+
# frontier-style step, and returns once it recovers or hits the step cap.
|
| 110 |
+
config.backtrack_mode = os.environ.get("SUDOKU_BACKTRACK_MODE", "prob")
|
| 111 |
+
config.backtrack_prob = float(os.environ.get("SUDOKU_BACKTRACK_PROB", 0.5))
|
| 112 |
+
config.backtrack_margin = float(os.environ.get("SUDOKU_BACKTRACK_MARGIN", 0.03))
|
| 113 |
+
config.backtrack_max_repair_steps = int(
|
| 114 |
+
os.environ.get("SUDOKU_BACKTRACK_MAX_REPAIR_STEPS", 6000))
|
| 115 |
+
# After leaving a repair episode, require this many frontier steps before
|
| 116 |
+
# another repair can trigger (prevents thrashing / starves-the-frontier).
|
| 117 |
+
config.backtrack_min_frontier_steps = int(
|
| 118 |
+
os.environ.get("SUDOKU_BACKTRACK_MIN_FRONTIER_STEPS", 0))
|
| 119 |
+
# While the frontier stage's own level accuracy is below this target, use a
|
| 120 |
+
# stricter (larger) repair trigger so stage-6 training is prioritized until
|
| 121 |
+
# it is itself "good". 0 disables.
|
| 122 |
+
config.backtrack_frontier_target_acc = float(
|
| 123 |
+
os.environ.get("SUDOKU_BACKTRACK_FRONTIER_TARGET_ACC", 0.0))
|
| 124 |
+
# Optional explicit graduation refs for inherited stages when resuming at
|
| 125 |
+
# stage > 1. Comma-separated floats for stages 1..start-1, e.g.
|
| 126 |
+
# "0.462,0.487,0.534,0.597,0.636". Empty = seed from first eval (old behavior).
|
| 127 |
+
config.backtrack_grad_acc_seed = os.environ.get("SUDOKU_GRAD_ACC_SEED", "")
|
| 128 |
+
# Max fraction of steps (since resume) that may be spent in repair mode.
|
| 129 |
+
# 0 disables the cap. Typical fair setting: 0.25.
|
| 130 |
+
config.backtrack_max_repair_fraction = float(
|
| 131 |
+
os.environ.get("SUDOKU_BACKTRACK_MAX_REPAIR_FRACTION", 0.0))
|
| 132 |
+
# Soft graduation refs: each eval, if current < grad, decay
|
| 133 |
+
# grad <- (1-d)*grad + d*current
|
| 134 |
+
# so chronic mild regression is forgiven; sharp drops still trigger.
|
| 135 |
+
# 0 disables (hard bars). Typical: 0.05–0.1.
|
| 136 |
+
config.backtrack_grad_decay = float(
|
| 137 |
+
os.environ.get("SUDOKU_BACKTRACK_GRAD_DECAY", 0.0))
|
| 138 |
+
# After this absolute step, force frontier-only (no new repairs).
|
| 139 |
+
# 0 disables. Use for "BT early, then freeze".
|
| 140 |
+
config.backtrack_freeze_after_step = int(
|
| 141 |
+
os.environ.get("SUDOKU_BACKTRACK_FREEZE_AFTER", 0))
|
| 142 |
+
# Frontier steps sample uniformly over ALL unlocked levels (standard-loop
|
| 143 |
+
# data mix) instead of only the frontier level. 1 = fair "standard+repairs"
|
| 144 |
+
# recipe (default); 0 = legacy frontier-level-only behavior.
|
| 145 |
+
config.backtrack_frontier_mix = int(
|
| 146 |
+
os.environ.get("SUDOKU_BACKTRACK_FRONTIER_MIX", 1))
|
| 147 |
+
config.curriculum_start_stage = int(os.environ.get("SUDOKU_START_STAGE", 1))
|
| 148 |
+
# Stage s = reasoning DEPTH s: s latent slots active, wave snapshots 1..s
|
| 149 |
+
# supervised. Difficulty is NOT gated -- every puzzle is available from
|
| 150 |
+
# step 0. Cap max_stage (e.g. =start_stage) to hold a single depth.
|
| 151 |
+
config.curriculum_max_stage = int(os.environ.get("SUDOKU_MAX_STAGE", 12))
|
| 152 |
+
# Promotion is gated on the candidate-set accuracy of the DEEPEST active
|
| 153 |
+
# slot, restricted to cells that changed from the previous snapshot.
|
| 154 |
+
# Instance arm: both gates required. mass = P(digit in raw candidate set S);
|
| 155 |
+
# spread = H(p|S)/log|S| on |S|>=2 cells. S is the wave-solver candidate
|
| 156 |
+
# set, not the filtered-instance support.
|
| 157 |
+
config.promote_acc_threshold = float(os.environ.get("SUDOKU_PROMOTE_ACC", 0.85))
|
| 158 |
+
config.promote_spread_threshold = float(
|
| 159 |
+
os.environ.get("SUDOKU_PROMOTE_SPREAD", 0.85))
|
| 160 |
+
# Location floor, applied to loc_wave. 0 = off.
|
| 161 |
+
config.promote_loc_threshold = float(
|
| 162 |
+
os.environ.get("SUDOKU_PROMOTE_LOC_WAVE", 0.0))
|
| 163 |
+
# 0 disables patience; instance arm never uses it regardless.
|
| 164 |
+
config.promote_patience_steps = int(os.environ.get("SUDOKU_PATIENCE", 0))
|
| 165 |
+
config.min_stage_steps = int(os.environ.get("SUDOKU_MIN_STAGE_STEPS", 2000))
|
| 166 |
+
# Plateau promotion: advance once the frontier depth stops improving, rather
|
| 167 |
+
# than on a fixed timer. A stage is "done" when it has not gained
|
| 168 |
+
# plateau_delta over its best accuracy for plateau_steps steps. This is the
|
| 169 |
+
# primary rule; the accuracy threshold is a fast path for mastery and
|
| 170 |
+
# promote_patience_steps is a hard cap so a stuck stage cannot stall
|
| 171 |
+
# training forever. Set plateau_steps=0 to disable.
|
| 172 |
+
config.plateau_steps = int(os.environ.get("SUDOKU_PLATEAU_STEPS", 20000))
|
| 173 |
+
config.plateau_delta = float(os.environ.get("SUDOKU_PLATEAU_DELTA", 0.005))
|
| 174 |
+
# Train-time difficulty balancing. 0 (default) = draw puzzles uniformly from
|
| 175 |
+
# the corpus, so the difficulty tag selects nothing. 1 = uniform over the 6
|
| 176 |
+
# levels, which upsamples level 8 from 1.8% to 16.7% of batches. Eval is
|
| 177 |
+
# always level-balanced so per-level accuracy stays measurable.
|
| 178 |
+
config.level_balanced_sampling = int(
|
| 179 |
+
os.environ.get("SUDOKU_LEVEL_BALANCED", 0))
|
| 180 |
+
# Data curriculum over the puzzle POOL (as opposed to the latent-depth
|
| 181 |
+
# curriculum over the supervision).
|
| 182 |
+
# "none" = every puzzle available from step 0.
|
| 183 |
+
# "rounds" = stage t admits only puzzles whose solver round count falls in
|
| 184 |
+
# the first t of max_stage equal-count bins (rounds span 5..38,
|
| 185 |
+
# so 12 bins give a genuinely smooth 12-step ladder). This is
|
| 186 |
+
# the same propagation-depth axis the latent arm supervises,
|
| 187 |
+
# which makes the two arms directly comparable. Needs
|
| 188 |
+
# SUDOKU_TRAIN_META / SUDOKU_TEST_META.
|
| 189 |
+
config.data_curriculum = os.environ.get("SUDOKU_DATA_CURRICULUM", "none")
|
| 190 |
+
config.train_meta_path = os.environ.get("SUDOKU_TRAIN_META", "") or None
|
| 191 |
+
config.test_meta_path = os.environ.get("SUDOKU_TEST_META", "") or None
|
| 192 |
+
|
| 193 |
+
# Model related parameters
|
| 194 |
+
config.block_size = 81
|
| 195 |
+
config.seq_len = 3 * config.block_size + config.num_latent_slots
|
| 196 |
+
config.vocab_size = 11
|
| 197 |
+
|
| 198 |
+
# Model architecture
|
| 199 |
+
config.num_heads = 8
|
| 200 |
+
config.num_layers = 8
|
| 201 |
+
config.emb_dim = 576
|
| 202 |
+
config.qkv_dim = 576
|
| 203 |
+
config.mlp_dim = 6 * config.emb_dim
|
| 204 |
+
config.dropout_rate = float(os.environ.get("SUDOKU_DROPOUT", 0.2))
|
| 205 |
+
config.attention_dropout_rate = float(
|
| 206 |
+
os.environ.get("SUDOKU_ATTN_DROPOUT",
|
| 207 |
+
os.environ.get("SUDOKU_DROPOUT", 0.2)))
|
| 208 |
+
|
| 209 |
+
# Training hyperparameters
|
| 210 |
+
config.learning_rate = float(os.environ.get("SUDOKU_LR", 0.0002)) # Base learning rate.
|
| 211 |
+
config.end_lr_factor = float(os.environ.get("SUDOKU_END_LR_FACTOR", 0.2))
|
| 212 |
+
config.warmup_tokens = int(os.environ.get("SUDOKU_WARMUP", 10000))
|
| 213 |
+
config.weight_decay = float(os.environ.get("SUDOKU_WD", 0.005))
|
| 214 |
+
# Resume from a checkpoint (set SUDOKU_RESUME=1 and pass --ckpt_loc=<path>).
|
| 215 |
+
config.resume_training = os.environ.get("SUDOKU_RESUME", "0") == "1"
|
| 216 |
+
|
| 217 |
+
# Other hyperparameters
|
| 218 |
+
config.seed = 7
|
| 219 |
+
config.save_checkpoint = os.environ.get("SUDOKU_SAVE_CKPT", "1") == "1"
|
| 220 |
+
config.save_every_steps = int(os.environ.get("SUDOKU_SAVE_EVERY", 10000))
|
| 221 |
+
# How many checkpoints to retain. Large default so per-stage checkpoints are
|
| 222 |
+
# never rolled off (disk is plentiful; ~0.5GB each).
|
| 223 |
+
config.ckpt_keep = int(os.environ.get("SUDOKU_CKPT_KEEP", 100))
|
| 224 |
+
config.use_wandb = False
|
| 225 |
+
config.wandb_project_name = 'sudoku'
|
| 226 |
+
|
| 227 |
+
# Evaluation related parameters
|
| 228 |
+
config.eval_every_steps = int(os.environ.get("SUDOKU_EVAL_EVERY", 2000))
|
| 229 |
+
config.eval_epochs = int(os.environ.get("SUDOKU_EVAL_EPOCHS", 5))
|
| 230 |
+
|
| 231 |
+
# Path to dataset
|
| 232 |
+
config.train_puzzle_path = os.environ.get(
|
| 233 |
+
"SUDOKU_TRAIN_PATH", "datasets/train_sudoku_puzzles.npy")
|
| 234 |
+
config.train_candidate_path = "datasets/train_sudoku_puzzles_candidate.npy"
|
| 235 |
+
config.test_puzzle_path = os.environ.get(
|
| 236 |
+
"SUDOKU_TEST_PATH", "datasets/test_sudoku_puzzles.npy")
|
| 237 |
+
config.test_candidate_path = "datasets/test_sudoku_puzzles_candidate.npy"
|
| 238 |
+
|
| 239 |
+
# Staged multi-candidate supervision (per-latent-slot BCE targets).
|
| 240 |
+
# Empty string disables cand-mask loading (useful for K=0 baselines).
|
| 241 |
+
config.train_cand_masks_path = os.environ.get(
|
| 242 |
+
"SUDOKU_TRAIN_CAND", "datasets_multicandidate/train_cand_masks.npy") or None
|
| 243 |
+
config.test_cand_masks_path = os.environ.get(
|
| 244 |
+
"SUDOKU_TEST_CAND", "datasets_multicandidate/test_cand_masks.npy") or None
|
| 245 |
+
# Superposition-instance targets. When set, the output prompt's value tokens
|
| 246 |
+
# come from one sampled stage-k assignment instead of the unique solution:
|
| 247 |
+
# the input prompt (clues) is fixed and the same puzzle recurs with different
|
| 248 |
+
# legal completions, so the candidate set is represented across the batch
|
| 249 |
+
# rather than supervised as a multi-hot set. Setting this should go with
|
| 250 |
+
# SUDOKU_AUX_WEIGHT=0 (candidate head off) -- the masks are then read only
|
| 251 |
+
# for the in-set metric. Empty string = classic single-solution targets.
|
| 252 |
+
config.instance_dir = os.environ.get("SUDOKU_INSTANCE_DIR", "") or None
|
| 253 |
+
# Weight of the auxiliary candidate-set BCE loss relative to the LM CE loss.
|
| 254 |
+
config.aux_cand_weight = float(os.environ.get("SUDOKU_AUX_WEIGHT", 1.0))
|
| 255 |
+
# Positive-class weight inside the candidate BCE (counters the sparsity of
|
| 256 |
+
# the multi-hot masks so the head doesn't collapse to predicting all-zeros).
|
| 257 |
+
config.aux_pos_weight = float(os.environ.get("SUDOKU_CAND_POS_WEIGHT", 5.0))
|
| 258 |
+
|
| 259 |
+
# How many latent slots each example activates.
|
| 260 |
+
# "level" = k = clip(level-2, 1, K). Difficulty-matched, but puzzle level
|
| 261 |
+
# explains only ~19% of the variance in solver round count, so
|
| 262 |
+
# most examples leave the majority of the K slots inert (a
|
| 263 |
+
# level-3 puzzle activates ONE slot for ~21 rounds of work).
|
| 264 |
+
# "depth" = k = num_passes, uniform over the batch. Every slot the
|
| 265 |
+
# recurrence actually fills is active and supervised, and the
|
| 266 |
+
# curriculum advances reasoning depth rather than puzzle level.
|
| 267 |
+
config.cand_slot_mode = os.environ.get("SUDOKU_CAND_SLOT_MODE", "depth")
|
| 268 |
+
# Latent passes granted per curriculum stage: num_passes = min(pps*stage, K).
|
| 269 |
+
# pps=2 with K=12 reaches all 12 slots by stage 6.
|
| 270 |
+
config.passes_per_stage = int(os.environ.get("SUDOKU_PASSES_PER_STAGE", 1))
|
| 271 |
+
# Superposition experiment: iterate every (puzzle, instance) pair of the
|
| 272 |
+
# pinned stage in shuffled epochs, so each instance of each puzzle is seen
|
| 273 |
+
# exactly `instance_epochs` times. 0 = old behaviour (draw a puzzle at
|
| 274 |
+
# random, then one of its assignments at random). instance_puzzles caps the
|
| 275 |
+
# puzzle count (0 = whole corpus) to shorten the experiment.
|
| 276 |
+
config.instance_epochs = int(os.environ.get("SUDOKU_INSTANCE_EPOCHS", 0))
|
| 277 |
+
config.instance_puzzles = int(os.environ.get("SUDOKU_INSTANCE_PUZZLES", 0))
|
| 278 |
+
# Preferred source: N instances per puzzle synthesized on the fly, each cell
|
| 279 |
+
# drawn uniformly from its candidate set, so the per-cell digit frequencies
|
| 280 |
+
# are equal and the CE optimum at that cell is the uniform superposition.
|
| 281 |
+
config.instance_uniform_draws = int(
|
| 282 |
+
os.environ.get("SUDOKU_UNIFORM_DRAWS", 0))
|
| 283 |
+
# Loss weight for candidate cells that did NOT change from the previous
|
| 284 |
+
# stage. Consecutive stages are highly redundant (at 12 stages ~97% of the
|
| 285 |
+
# target bits are copies of the previous slot), so plain BCE is dominated by
|
| 286 |
+
# echoing the previous slot. <1.0 down-weights the copied cells and puts the
|
| 287 |
+
# gradient on the digits actually eliminated at this stage. 1.0 = off.
|
| 288 |
+
config.aux_delta_bg = float(os.environ.get("SUDOKU_CAND_DELTA_BG", 1.0))
|
| 289 |
+
|
| 290 |
+
return config
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def main(argv):
|
| 294 |
+
if len(argv) > 1:
|
| 295 |
+
raise app.UsageError('Too many command-line arguments.')
|
| 296 |
+
|
| 297 |
+
# # Hide any GPUs from TensorFlow. Otherwise TF might reserve memory and make
|
| 298 |
+
# # it unavailable to JAX.
|
| 299 |
+
tf.config.experimental.set_visible_devices([], 'GPU')
|
| 300 |
+
|
| 301 |
+
cfgs = get_config()
|
| 302 |
+
if cfgs.resume_training:
|
| 303 |
+
assert _CKPT_LOC.value is not None
|
| 304 |
+
|
| 305 |
+
if cfgs.use_wandb:
|
| 306 |
+
wandb.init(project=cfgs.wandb_project_name, name=_EXP_NAME.value, config=cfgs)
|
| 307 |
+
|
| 308 |
+
logging.info('JAX process: %d / %d', jax.process_index(), jax.process_count())
|
| 309 |
+
logging.info('JAX local devices: %r', jax.local_devices())
|
| 310 |
+
|
| 311 |
+
# Add a note so that we can tell which task is which JAX host.
|
| 312 |
+
# (Depending on the platform task 0 is not guaranteed to be host 0)
|
| 313 |
+
platform.work_unit().set_task_status(f'process_index: {jax.process_index()}, '
|
| 314 |
+
f'process_count: {jax.process_count()}')
|
| 315 |
+
platform.work_unit().create_artifact(platform.ArtifactType.DIRECTORY,
|
| 316 |
+
_WORKDIR.value, 'workdir')
|
| 317 |
+
|
| 318 |
+
logging.info(cfgs)
|
| 319 |
+
|
| 320 |
+
cfgs.workdir = _WORKDIR.value
|
| 321 |
+
cfgs.ckpt_loc = _CKPT_LOC.value
|
| 322 |
+
if int(getattr(cfgs, "backtrack", 0)):
|
| 323 |
+
if str(getattr(cfgs, "backtrack_mode", "prob")) == "adaptive":
|
| 324 |
+
train_backtrack.train_and_evaluate_backtrack_adaptive(cfgs, _WORKDIR.value)
|
| 325 |
+
else:
|
| 326 |
+
train_backtrack.train_and_evaluate_backtrack(cfgs, _WORKDIR.value)
|
| 327 |
+
else:
|
| 328 |
+
train_and_evaluate.train_and_evaluate(cfgs, _WORKDIR.value)
|
| 329 |
+
|
| 330 |
+
if cfgs.use_wandb:
|
| 331 |
+
wandb.finish()
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
if __name__ == '__main__':
|
| 335 |
+
jax.config.config_with_absl()
|
| 336 |
+
app.run(main)
|
code/wavecurriculum_run/train/model.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2024 The Google Research Authors.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""Model Architecture."""
|
| 17 |
+
|
| 18 |
+
import functools
|
| 19 |
+
from typing import Any, Callable
|
| 20 |
+
|
| 21 |
+
from flax import linen as nn
|
| 22 |
+
from flax import struct
|
| 23 |
+
from jax import numpy as jnp
|
| 24 |
+
|
| 25 |
+
@struct.dataclass
|
| 26 |
+
class TransformerConfig:
|
| 27 |
+
"""Global hyperparameters used to minimize obnoxious kwarg plumbing."""
|
| 28 |
+
vocab_size: int = 1
|
| 29 |
+
dtype: Any = jnp.float32
|
| 30 |
+
emb_dim: int = 512
|
| 31 |
+
num_heads: int = 8
|
| 32 |
+
num_layers: int = 6
|
| 33 |
+
qkv_dim: int = 512
|
| 34 |
+
mlp_dim: int = 2048
|
| 35 |
+
seq_len: int = 2048 # Maximum sequence length
|
| 36 |
+
dropout_rate: float = 0.1
|
| 37 |
+
attention_dropout_rate: float = 0.1
|
| 38 |
+
deterministic: bool = False
|
| 39 |
+
num_latent_slots: int = 0 # K continuous latent thought slots
|
| 40 |
+
# When False, the fed-back latent thought vectors are NOT injected into the
|
| 41 |
+
# slot positions (they keep their placeholder token embedding). This turns
|
| 42 |
+
# the K slots into static, parallel per-stage readouts with NO carried
|
| 43 |
+
# recurrent state -- the "stagewise supervision, no recurrence" control.
|
| 44 |
+
inject_latents: bool = True
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class TransformerBlock(nn.Module):
|
| 48 |
+
config: Any = None
|
| 49 |
+
|
| 50 |
+
def setup(self):
|
| 51 |
+
self.vocab_size = self.config.vocab_size
|
| 52 |
+
self.emb_dim = self.config.emb_dim
|
| 53 |
+
self.num_layers = self.config.num_layers
|
| 54 |
+
|
| 55 |
+
@nn.compact
|
| 56 |
+
def __call__(self, inputs, causal_mask_inputs, training=True):
|
| 57 |
+
"""
|
| 58 |
+
Transformer Block call function.
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
inputs: Input tensor.
|
| 62 |
+
causal_mask_inputs: Causal mask for the inputs.
|
| 63 |
+
training: Whether the model is in training mode.
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
Transformed tensor after self-attention and MLP layers.
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
x = inputs + nn.SelfAttention(
|
| 70 |
+
num_heads=self.config.num_heads, dtype=self.config.dtype,
|
| 71 |
+
qkv_features=self.config.qkv_dim,
|
| 72 |
+
kernel_init=nn.initializers.xavier_uniform(),
|
| 73 |
+
bias_init=nn.initializers.normal(stddev=1e-6),
|
| 74 |
+
use_bias=False, broadcast_dropout=False,
|
| 75 |
+
dropout_rate=self.config.attention_dropout_rate, normalize_qk=True,
|
| 76 |
+
deterministic=self.config.deterministic)(inputs, causal_mask_inputs)
|
| 77 |
+
|
| 78 |
+
def mlp(x):
|
| 79 |
+
"""
|
| 80 |
+
Multi-Layer Perceptron function.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
x: Input tensor.
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
Transformed tensor after applying MLP layers.
|
| 87 |
+
"""
|
| 88 |
+
dense_with_init = functools.partial(
|
| 89 |
+
nn.Dense,
|
| 90 |
+
kernel_init=nn.initializers.xavier_uniform(),
|
| 91 |
+
bias_init=nn.initializers.normal(stddev=1e-6)
|
| 92 |
+
)
|
| 93 |
+
x = dense_with_init(features=self.config.mlp_dim)(x)
|
| 94 |
+
x = nn.gelu(x)
|
| 95 |
+
x = dense_with_init(features=self.config.emb_dim)(x)
|
| 96 |
+
x = nn.Dropout(rate=self.config.dropout_rate,
|
| 97 |
+
deterministic=self.config.deterministic)(x)
|
| 98 |
+
return x
|
| 99 |
+
|
| 100 |
+
x = x + mlp(x)
|
| 101 |
+
return x
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class TransformerLMHeadModel(nn.Module):
|
| 105 |
+
config: Any = None
|
| 106 |
+
|
| 107 |
+
def setup(self):
|
| 108 |
+
self.vocab_size = self.config.vocab_size
|
| 109 |
+
self.emb_dim = self.config.emb_dim
|
| 110 |
+
self.num_layers = self.config.num_layers
|
| 111 |
+
|
| 112 |
+
@nn.compact
|
| 113 |
+
def __call__(self, inputs, latent_values=None, latent_positions=None,
|
| 114 |
+
latent_active=None, training=True):
|
| 115 |
+
"""
|
| 116 |
+
Transformer LM Head call function.
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
inputs: Input token ids (batch, seq).
|
| 120 |
+
latent_values: Optional (batch, K, emb_dim) continuous thought
|
| 121 |
+
vectors (raw last-layer hiddens fed back, Coconut/ATC style).
|
| 122 |
+
latent_positions: Optional (batch, K) int positions of the latent
|
| 123 |
+
slots in the sequence (per-example, after the clue block).
|
| 124 |
+
latent_active: Optional (batch, K) bool; slot j uses the projected
|
| 125 |
+
latent vector when True, otherwise keeps the placeholder
|
| 126 |
+
token embedding.
|
| 127 |
+
training: Whether the model is in training mode.
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
(logits, hidden): LM logits and final (post-LayerNorm) hidden
|
| 131 |
+
states, the latter used to build the next continuous thought.
|
| 132 |
+
"""
|
| 133 |
+
batch_size, seq_size = inputs.shape
|
| 134 |
+
|
| 135 |
+
causal_mask_x = nn.make_causal_mask(inputs, dtype=self.config.dtype)
|
| 136 |
+
|
| 137 |
+
# Embed the input tensor using a learnable embedding matrix.
|
| 138 |
+
embed_with_init = functools.partial(
|
| 139 |
+
nn.Embed, embedding_init=nn.initializers.normal(stddev=0.02))
|
| 140 |
+
token_embeddings = embed_with_init(
|
| 141 |
+
num_embeddings=self.config.vocab_size,
|
| 142 |
+
features=self.config.emb_dim,
|
| 143 |
+
)(inputs)
|
| 144 |
+
|
| 145 |
+
# Check the shape of the embedded tensor.
|
| 146 |
+
assert token_embeddings.shape == (batch_size, seq_size,
|
| 147 |
+
self.config.emb_dim)
|
| 148 |
+
|
| 149 |
+
# Continuous latent thoughts: project fed-back hidden states and
|
| 150 |
+
# scatter them into the latent slot positions, replacing the
|
| 151 |
+
# placeholder token embedding (position embeddings still added below).
|
| 152 |
+
# Skipped when inject_latents is False (no-recurrence control: the slots
|
| 153 |
+
# stay as static placeholders and the candidate heads become parallel
|
| 154 |
+
# per-stage readouts with no carried state).
|
| 155 |
+
if latent_values is not None and self.config.inject_latents:
|
| 156 |
+
proj = nn.Dense(features=self.config.emb_dim,
|
| 157 |
+
kernel_init=nn.initializers.xavier_uniform(),
|
| 158 |
+
name="latent_proj_in")(latent_values)
|
| 159 |
+
proj = nn.gelu(proj)
|
| 160 |
+
proj = nn.Dense(features=self.config.emb_dim,
|
| 161 |
+
kernel_init=nn.initializers.xavier_uniform(),
|
| 162 |
+
name="latent_proj_out")(proj)
|
| 163 |
+
bidx = jnp.arange(batch_size)[:, None]
|
| 164 |
+
cur = token_embeddings[bidx, latent_positions]
|
| 165 |
+
new = jnp.where(latent_active[..., None],
|
| 166 |
+
proj.astype(cur.dtype), cur)
|
| 167 |
+
token_embeddings = token_embeddings.at[
|
| 168 |
+
bidx, latent_positions].set(new)
|
| 169 |
+
|
| 170 |
+
# Initialize the positional embedding variable.
|
| 171 |
+
pos_embedding_variable = self.variable(
|
| 172 |
+
"params",
|
| 173 |
+
"position_embeddings",
|
| 174 |
+
jnp.zeros,
|
| 175 |
+
(self.config.seq_len, self.config.emb_dim),
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
# Slice the positional embedding array to the correct sequence length.
|
| 179 |
+
pos_embeddings = pos_embedding_variable.value[:seq_size, :]
|
| 180 |
+
|
| 181 |
+
# Check the shape of the positional embedding array.
|
| 182 |
+
output_tuple = (pos_embeddings.shape, token_embeddings.shape[1:])
|
| 183 |
+
assert pos_embeddings.shape == token_embeddings.shape[1:], output_tuple
|
| 184 |
+
|
| 185 |
+
# Add the positional embeddings to the token embeddings.
|
| 186 |
+
x = token_embeddings + pos_embeddings[None, :, :]
|
| 187 |
+
|
| 188 |
+
# Apply dropout to the input.
|
| 189 |
+
x = nn.Dropout(rate=self.config.dropout_rate,
|
| 190 |
+
deterministic=self.config.deterministic)(x)
|
| 191 |
+
|
| 192 |
+
# Apply the Transformer layers. remat (gradient checkpointing) keeps
|
| 193 |
+
# the multi-pass latent recurrence within GPU memory under full BPTT.
|
| 194 |
+
RematBlock = nn.remat(TransformerBlock)
|
| 195 |
+
for i in range(self.num_layers):
|
| 196 |
+
x = RematBlock(config=self.config)(
|
| 197 |
+
x, causal_mask_x, training=training)
|
| 198 |
+
|
| 199 |
+
self.sow('intermediates', 'feature_' + str(i), x)
|
| 200 |
+
|
| 201 |
+
# Apply the final layer normalization.
|
| 202 |
+
x = nn.LayerNorm()(x)
|
| 203 |
+
|
| 204 |
+
# Apply the LM head.
|
| 205 |
+
logits = nn.Dense(features=self.config.vocab_size,
|
| 206 |
+
kernel_init=nn.initializers.xavier_uniform(),
|
| 207 |
+
bias_init=nn.initializers.normal(stddev=1e-6),
|
| 208 |
+
use_bias=False)(x)
|
| 209 |
+
|
| 210 |
+
# Check the shape of the output tensor.
|
| 211 |
+
assert logits.shape == (batch_size, seq_size, self.config.vocab_size)
|
| 212 |
+
|
| 213 |
+
# ---- Auxiliary multi-candidate head ----
|
| 214 |
+
# Each latent slot reads its (post-LayerNorm) hidden and predicts the
|
| 215 |
+
# full 81x9 candidate grid for its reasoning stage. Trained with BCE
|
| 216 |
+
# (independent per-digit sigmoids = candidate-set membership), NOT
|
| 217 |
+
# softmax, so multiple digits can be "on" at intermediate stages.
|
| 218 |
+
cand_logits = None
|
| 219 |
+
if latent_positions is not None:
|
| 220 |
+
bidx = jnp.arange(batch_size)[:, None]
|
| 221 |
+
slot_hidden = x[bidx, latent_positions] # (bs, K, emb)
|
| 222 |
+
h = nn.Dense(features=self.config.emb_dim,
|
| 223 |
+
kernel_init=nn.initializers.xavier_uniform(),
|
| 224 |
+
name="cand_head_in")(slot_hidden)
|
| 225 |
+
h = nn.gelu(h)
|
| 226 |
+
cand_logits = nn.Dense(features=81 * 9,
|
| 227 |
+
kernel_init=nn.initializers.xavier_uniform(),
|
| 228 |
+
name="cand_head_out")(h) # (bs, K, 729)
|
| 229 |
+
cand_logits = cand_logits.reshape(
|
| 230 |
+
batch_size, -1, 81, 9) # (bs, K, 81, 9)
|
| 231 |
+
|
| 232 |
+
return logits, x, cand_logits
|
code/wavecurriculum_run/train/train_and_evaluate.py
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2024 The Google Research Authors.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""This file contains function that coordinates the training and evaluation of the model."""
|
| 17 |
+
|
| 18 |
+
import functools
|
| 19 |
+
import json
|
| 20 |
+
import math
|
| 21 |
+
import os
|
| 22 |
+
import signal
|
| 23 |
+
import socket
|
| 24 |
+
import time
|
| 25 |
+
import traceback
|
| 26 |
+
|
| 27 |
+
from absl import logging
|
| 28 |
+
from clu import metric_writers
|
| 29 |
+
from flax import jax_utils
|
| 30 |
+
from flax import linen as nn
|
| 31 |
+
from flax.training import checkpoints
|
| 32 |
+
import jax
|
| 33 |
+
from jax import random
|
| 34 |
+
import jax.numpy as jnp
|
| 35 |
+
import numpy as np
|
| 36 |
+
import tensorflow as tf
|
| 37 |
+
import wandb
|
| 38 |
+
|
| 39 |
+
from train import data
|
| 40 |
+
from train import evaluater
|
| 41 |
+
from train import model
|
| 42 |
+
from train import trainer
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def log_hyperparams_tb(
|
| 47 |
+
config, model_config, initial_variables, tf_summary_writer
|
| 48 |
+
):
|
| 49 |
+
"""Log hyperparameters to TensorBoard.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
config: experiment's ConfigDict
|
| 53 |
+
model_config: model's ConfigDict
|
| 54 |
+
initial_variables: initial hyperparameter values
|
| 55 |
+
tf_summary_writer: SummaryWriter object.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
The SummaryWriter object and the config.
|
| 59 |
+
"""
|
| 60 |
+
# Calculate the total number of model parameters
|
| 61 |
+
config.num_model_parameters = sum(
|
| 62 |
+
x.size for x in jax.tree_util.tree_leaves(initial_variables)
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Convert hyperparameters to tensors
|
| 66 |
+
config_hyperparameters = [
|
| 67 |
+
tf.convert_to_tensor([k, str(v)]) for k, v in config.items()
|
| 68 |
+
]
|
| 69 |
+
model_config_hyperparameters = [
|
| 70 |
+
tf.convert_to_tensor([k, str(v)])
|
| 71 |
+
for k, v in model_config.__dict__.items()
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
# Log model hyperparameters to TensorBoard
|
| 75 |
+
with tf_summary_writer.as_default():
|
| 76 |
+
tf.summary.text(
|
| 77 |
+
"Model hyperparameters", tf.stack(model_config_hyperparameters), step=0
|
| 78 |
+
)
|
| 79 |
+
tf.summary.text(
|
| 80 |
+
"Config hyperparameters", tf.stack(config_hyperparameters), step=0
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
return tf_summary_writer, config
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _write_json(path, payload):
|
| 89 |
+
"""Atomic JSON write so a sidecar never reads a half-written file."""
|
| 90 |
+
tmp = path + ".tmp"
|
| 91 |
+
with open(tmp, "w") as f:
|
| 92 |
+
json.dump(payload, f, indent=2, sort_keys=True)
|
| 93 |
+
f.write("\n")
|
| 94 |
+
os.replace(tmp, path)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _write_heartbeat(workdir, payload):
|
| 98 |
+
payload = dict(payload)
|
| 99 |
+
payload.setdefault("host", socket.gethostname())
|
| 100 |
+
payload.setdefault("job_id", os.environ.get("SLURM_JOB_ID", ""))
|
| 101 |
+
payload["unix_time"] = time.time()
|
| 102 |
+
payload["iso_time"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
| 103 |
+
slim = {k: payload[k] for k in
|
| 104 |
+
("event", "step", "stage", "loss",
|
| 105 |
+
# val_acc = P(digit not in the stage-t candidate set)
|
| 106 |
+
"val_acc", "puzzle_acc",
|
| 107 |
+
# location order, tolerant to ties within a propagation wave
|
| 108 |
+
"loc_wave", "loc_coverage", "loc_lcs", "loc_dup", "loc_acc",
|
| 109 |
+
# superposition: excess 0 == uniform over the candidate set
|
| 110 |
+
"val_excess", "val_mass", "val_spread",
|
| 111 |
+
"val_excess_multi", "val_mass_multi", "val_out_multi",
|
| 112 |
+
"val_kl_multi",
|
| 113 |
+
"last_ckpt_step", "error")
|
| 114 |
+
if k in payload}
|
| 115 |
+
_write_json(os.path.join(workdir, "heartbeat.json"), slim)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def train_and_evaluate(config, workdir):
|
| 119 |
+
"""The training and evaluation loops for the model.
|
| 120 |
+
|
| 121 |
+
Args:
|
| 122 |
+
config: experiment's config dictionary.
|
| 123 |
+
workdir: directory to use for logging.
|
| 124 |
+
"""
|
| 125 |
+
# Orbax checkpointing requires an absolute path.
|
| 126 |
+
workdir = os.path.abspath(workdir)
|
| 127 |
+
|
| 128 |
+
logging.info("Creating training and evaluator dataset iterator")
|
| 129 |
+
curriculum = data.CurriculumState(
|
| 130 |
+
stage=int(getattr(config, "curriculum_start_stage", 1)),
|
| 131 |
+
max_stage=int(getattr(config, "curriculum_max_stage", 6)))
|
| 132 |
+
train_data_iter = data.create_iter(
|
| 133 |
+
config, config.minibatch_size, train=True, curriculum=curriculum)
|
| 134 |
+
eval_data_iter = data.create_iter(config, config.minibatch_size, train=False)
|
| 135 |
+
|
| 136 |
+
logging.info("Finished creating training dataset iterator")
|
| 137 |
+
|
| 138 |
+
model_config = model.TransformerConfig(
|
| 139 |
+
dtype=config.dtype,
|
| 140 |
+
vocab_size=config.vocab_size,
|
| 141 |
+
seq_len=config.seq_len,
|
| 142 |
+
num_heads=config.num_heads,
|
| 143 |
+
num_layers=config.num_layers,
|
| 144 |
+
emb_dim=config.emb_dim,
|
| 145 |
+
qkv_dim=config.qkv_dim,
|
| 146 |
+
mlp_dim=config.mlp_dim,
|
| 147 |
+
dropout_rate=config.dropout_rate,
|
| 148 |
+
attention_dropout_rate=config.attention_dropout_rate,
|
| 149 |
+
deterministic=False,
|
| 150 |
+
num_latent_slots=int(config.num_latent_slots),
|
| 151 |
+
inject_latents=bool(int(getattr(config, "recurrent_latent", 1))),
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
logging.info("train_config: %s", str(model_config.__dict__))
|
| 155 |
+
print(str(model_config.__dict__), flush=True)
|
| 156 |
+
|
| 157 |
+
rng = jax.random.PRNGKey(config.seed)
|
| 158 |
+
rng, init_rng, inference_rng = random.split(rng, num=3)
|
| 159 |
+
|
| 160 |
+
# Initialize the model and get initial variables. Dummy latent arguments
|
| 161 |
+
# are provided so the latent projector parameters are created at init.
|
| 162 |
+
rng, dropout_rng = jax.random.split(rng)
|
| 163 |
+
input_shape = (config.minibatch_size, config.seq_len)
|
| 164 |
+
net = model.TransformerLMHeadModel(model_config)
|
| 165 |
+
rng_keys = {"params": init_rng, "dropout": dropout_rng}
|
| 166 |
+
K = int(config.num_latent_slots)
|
| 167 |
+
dummy_latents = jnp.zeros(
|
| 168 |
+
(config.minibatch_size, K, config.emb_dim), model_config.dtype)
|
| 169 |
+
dummy_positions = jnp.zeros((config.minibatch_size, K), jnp.int32)
|
| 170 |
+
dummy_active = jnp.zeros((config.minibatch_size, K), bool)
|
| 171 |
+
sample_out, initial_variables = jax.jit(
|
| 172 |
+
net.init_with_output
|
| 173 |
+
)(rng_keys, jnp.ones(input_shape, jnp.int32), dummy_latents,
|
| 174 |
+
dummy_positions, dummy_active)
|
| 175 |
+
|
| 176 |
+
state, lr_scheduler_fn = trainer.get_state(config, net, initial_variables)
|
| 177 |
+
# Resume-and-extend support: when resuming, start the training loop at the
|
| 178 |
+
# restored optimizer step (not 0) so a larger config.max_steps continues the
|
| 179 |
+
# cosine LR schedule cleanly instead of re-running steps or overshooting.
|
| 180 |
+
start_step = 0
|
| 181 |
+
if config.resume_training:
|
| 182 |
+
state = checkpoints.restore_checkpoint(config.ckpt_loc, state)
|
| 183 |
+
start_step = int(state.step)
|
| 184 |
+
print("----------Restored model from", config.ckpt_loc,
|
| 185 |
+
f"at step {start_step}-----------")
|
| 186 |
+
|
| 187 |
+
writer = metric_writers.create_default_writer(
|
| 188 |
+
workdir, asynchronous=False, just_logging=(jax.process_index() > 0))
|
| 189 |
+
tf_summary_writer = tf.summary.create_file_writer(workdir)
|
| 190 |
+
|
| 191 |
+
logging.info("config: %s", str(config.__dict__))
|
| 192 |
+
state = jax_utils.replicate(state)
|
| 193 |
+
|
| 194 |
+
dropout_rngs = jax.random.split(rng, jax.local_device_count())
|
| 195 |
+
|
| 196 |
+
def make_p_train_step(num_passes):
|
| 197 |
+
return jax.pmap(
|
| 198 |
+
functools.partial(
|
| 199 |
+
trainer.train_step,
|
| 200 |
+
config=model_config,
|
| 201 |
+
hyperparams=config,
|
| 202 |
+
learning_rate_fn=lr_scheduler_fn,
|
| 203 |
+
num_passes=num_passes),
|
| 204 |
+
axis_name="batch",
|
| 205 |
+
donate_argnums=(0,))
|
| 206 |
+
|
| 207 |
+
# num_passes = latent recurrence depth for this stage, capped by the number
|
| 208 |
+
# of latent slots (K=0 -> 0 passes -> no-latent control baseline).
|
| 209 |
+
# When recurrence is disabled (control), force 0 passes so no hidden state
|
| 210 |
+
# is ever fed back: the slots become static, independent per-stage readouts.
|
| 211 |
+
recurrent = bool(int(getattr(config, "recurrent_latent", 1)))
|
| 212 |
+
passes_per_stage = int(getattr(config, "passes_per_stage", 1))
|
| 213 |
+
def passes_for(stage):
|
| 214 |
+
return min(passes_per_stage * stage, K) if recurrent else 0
|
| 215 |
+
p_train_step = make_p_train_step(passes_for(curriculum.stage))
|
| 216 |
+
|
| 217 |
+
p_eval_step = jax.pmap(functools.partial(evaluater.eval_step,
|
| 218 |
+
config=model_config.replace(deterministic=True)),
|
| 219 |
+
axis_name="batch")
|
| 220 |
+
|
| 221 |
+
hooks, report_progress, train_metrics = trainer.get_metrics_report_progress(
|
| 222 |
+
config, workdir, writer)
|
| 223 |
+
|
| 224 |
+
tf_summary_writer, config = log_hyperparams_tb(
|
| 225 |
+
config, model_config, initial_variables, tf_summary_writer
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
promote_threshold = float(getattr(config, "promote_acc_threshold", 0.85))
|
| 229 |
+
# Applies to loc_wave (order up to wave ties), not the positional loc_acc.
|
| 230 |
+
# 0 disables the gate.
|
| 231 |
+
promote_loc_threshold = float(getattr(config, "promote_loc_threshold", 0.0))
|
| 232 |
+
# Max tolerated val_excess = CE(Uniform(S) || p) - log|S|. 0 disables.
|
| 233 |
+
promote_excess_threshold = float(
|
| 234 |
+
getattr(config, "promote_val_excess_threshold", 0.0))
|
| 235 |
+
promote_patience = int(getattr(config, "promote_patience_steps", 8000))
|
| 236 |
+
min_stage_steps = int(getattr(config, "min_stage_steps", 2000))
|
| 237 |
+
plateau_steps = int(getattr(config, "plateau_steps", 0))
|
| 238 |
+
plateau_delta = float(getattr(config, "plateau_delta", 0.005))
|
| 239 |
+
instance_mode = bool(getattr(config, "instance_dir", None))
|
| 240 |
+
per_stage_inset = {}
|
| 241 |
+
per_stage_vexcess, per_stage_vmass, per_stage_vspread = {}, {}, {}
|
| 242 |
+
per_stage_vexc_m, per_stage_vmass_m, per_stage_vkl_m = {}, {}, {}
|
| 243 |
+
# Best frontier-depth accuracy seen in the current stage, and the step it
|
| 244 |
+
# was last improved: the plateau detector's state.
|
| 245 |
+
stage_best_acc = -1.0
|
| 246 |
+
stage_best_step = start_step
|
| 247 |
+
ckpt_keep = int(getattr(config, "ckpt_keep", 100))
|
| 248 |
+
# Protected directory for per-stage checkpoints (never rolled off).
|
| 249 |
+
stage_ckpt_dir = os.path.join(workdir, "stage_ckpts")
|
| 250 |
+
stage_started_at = start_step
|
| 251 |
+
last_ckpt_step = start_step
|
| 252 |
+
stop_requested = {"flag": False}
|
| 253 |
+
|
| 254 |
+
def _on_stop(signum, _frame):
|
| 255 |
+
print(f"[signal] {signum} received; will save and exit after this step",
|
| 256 |
+
flush=True)
|
| 257 |
+
stop_requested["flag"] = True
|
| 258 |
+
|
| 259 |
+
signal.signal(signal.SIGTERM, _on_stop)
|
| 260 |
+
signal.signal(signal.SIGINT, _on_stop)
|
| 261 |
+
_write_heartbeat(workdir, {
|
| 262 |
+
"event": "start",
|
| 263 |
+
"step": start_step,
|
| 264 |
+
"stage": curriculum.stage,
|
| 265 |
+
"max_steps": int(config.max_steps),
|
| 266 |
+
})
|
| 267 |
+
|
| 268 |
+
with metric_writers.ensure_flushes(writer):
|
| 269 |
+
for step in range(start_step, config.max_steps):
|
| 270 |
+
if step%10000 == 0:
|
| 271 |
+
print("Step:", step, flush=True)
|
| 272 |
+
|
| 273 |
+
state, metrics = trainer.train_one_step(p_train_step, config, state,
|
| 274 |
+
step, dropout_rngs, train_data_iter)
|
| 275 |
+
|
| 276 |
+
for h in hooks:
|
| 277 |
+
h(step)
|
| 278 |
+
|
| 279 |
+
if math.isnan(metrics["loss"][0]):
|
| 280 |
+
print("The loss function became nan: This might be due to the choice of hyperparameters.")
|
| 281 |
+
_write_heartbeat(workdir, {
|
| 282 |
+
"event": "nan_loss", "step": step, "stage": curriculum.stage,
|
| 283 |
+
})
|
| 284 |
+
break
|
| 285 |
+
|
| 286 |
+
if step % config.eval_every_steps == 0:
|
| 287 |
+
try:
|
| 288 |
+
eval_metrics = evaluater.get_eval_metrics(
|
| 289 |
+
state, eval_data_iter, p_eval_step, config)
|
| 290 |
+
except Exception:
|
| 291 |
+
traceback.print_exc()
|
| 292 |
+
_write_heartbeat(workdir, {
|
| 293 |
+
"event": "eval_error",
|
| 294 |
+
"step": step,
|
| 295 |
+
"stage": curriculum.stage,
|
| 296 |
+
"loss": float(metrics["loss"].mean()),
|
| 297 |
+
"ce": float(metrics["ce_loss"].mean()),
|
| 298 |
+
"error": traceback.format_exc()[-2000:],
|
| 299 |
+
})
|
| 300 |
+
print("[eval] failed; skipping this eval and continuing",
|
| 301 |
+
flush=True)
|
| 302 |
+
eval_metrics = None
|
| 303 |
+
if eval_metrics is not None:
|
| 304 |
+
per_level = eval_metrics.pop("per_level_acc")
|
| 305 |
+
per_slot = eval_metrics.pop("per_slot_acc", {})
|
| 306 |
+
per_slot_ch = eval_metrics.pop("per_slot_acc_changed", {})
|
| 307 |
+
per_stage_inset = eval_metrics.pop("per_stage_inset_acc", {})
|
| 308 |
+
per_stage_vexcess = eval_metrics.pop(
|
| 309 |
+
"per_stage_val_excess", {})
|
| 310 |
+
per_stage_vmass = eval_metrics.pop("per_stage_val_mass", {})
|
| 311 |
+
per_stage_vspread = eval_metrics.pop(
|
| 312 |
+
"per_stage_val_spread", {})
|
| 313 |
+
per_stage_vexc_m = eval_metrics.pop(
|
| 314 |
+
"per_stage_val_excess_multi", {})
|
| 315 |
+
per_stage_vmass_m = eval_metrics.pop(
|
| 316 |
+
"per_stage_val_mass_multi", {})
|
| 317 |
+
per_stage_vkl_m = eval_metrics.pop(
|
| 318 |
+
"per_stage_val_kl_multi", {})
|
| 319 |
+
eval_metrics.pop("per_stage_val_ce", {})
|
| 320 |
+
eval_metrics.pop("per_stage_val_floor", {})
|
| 321 |
+
per_bin = eval_metrics.pop("per_bin_acc", {})
|
| 322 |
+
def _m(key):
|
| 323 |
+
v = eval_metrics.get(key, [])
|
| 324 |
+
return round(float(np.mean(v)), 4) if len(v) else -1.0
|
| 325 |
+
def _s(d):
|
| 326 |
+
return round(float(d.get(curriculum.stage, -1.0)), 4)
|
| 327 |
+
# val_acc = P(digit not in stage-t candidate set),
|
| 328 |
+
# teacher-forced so location cannot contaminate it.
|
| 329 |
+
# 0 = all mass inside S; chance at stage 0 is ~0.59.
|
| 330 |
+
mass = _s(per_stage_vmass)
|
| 331 |
+
val_out = round(1.0 - mass, 4) if mass >= 0 else -1.0
|
| 332 |
+
mass_m = _s(per_stage_vmass_m)
|
| 333 |
+
val_out_m = round(1.0 - mass_m, 4) if mass_m >= 0 else -1.0
|
| 334 |
+
print(step,
|
| 335 |
+
"stage", curriculum.stage,
|
| 336 |
+
"loss", round(float(metrics["loss"].mean()), 4),
|
| 337 |
+
"val_acc", val_out,
|
| 338 |
+
"loc_wave", _m("loc_wave"),
|
| 339 |
+
"loc_cov", _m("loc_coverage"),
|
| 340 |
+
# superposition on |S|>=2 cells: excess = leak + kl
|
| 341 |
+
"excess*", _s(per_stage_vexc_m),
|
| 342 |
+
"out*", val_out_m,
|
| 343 |
+
"kl*", _s(per_stage_vkl_m),
|
| 344 |
+
"val_spread", _s(per_stage_vspread),
|
| 345 |
+
flush=True)
|
| 346 |
+
_write_heartbeat(workdir, {
|
| 347 |
+
"event": "eval",
|
| 348 |
+
"step": int(step),
|
| 349 |
+
"stage": int(curriculum.stage),
|
| 350 |
+
"loss": round(float(metrics["loss"].mean()), 6),
|
| 351 |
+
"ce": round(float(metrics["ce_loss"].mean()), 6),
|
| 352 |
+
"val_acc": val_out,
|
| 353 |
+
"puzzle_acc": _m("acc_complete_puzzle"),
|
| 354 |
+
# Permutation-tolerant location signals; loc_acc is the
|
| 355 |
+
# old positional match, kept only for continuity.
|
| 356 |
+
"loc_wave": _m("loc_wave"),
|
| 357 |
+
"loc_coverage": _m("loc_coverage"),
|
| 358 |
+
"loc_lcs": _m("loc_lcs"),
|
| 359 |
+
"loc_dup": _m("loc_dup"),
|
| 360 |
+
"loc_acc": _m("loc_acc"),
|
| 361 |
+
# Superposition: 0 excess == uniform over the stage's
|
| 362 |
+
# candidate set; spread 1.0 == no mode collapse.
|
| 363 |
+
"val_excess": _s(per_stage_vexcess),
|
| 364 |
+
"val_mass": _s(per_stage_vmass),
|
| 365 |
+
"val_spread": _s(per_stage_vspread),
|
| 366 |
+
# |S|>=2 cells only: excess_multi == log(1/mass_multi)
|
| 367 |
+
# + kl_multi, so it is the single number that falls only
|
| 368 |
+
# when leakage and non-uniformity both fall.
|
| 369 |
+
"val_excess_multi": _s(per_stage_vexc_m),
|
| 370 |
+
"val_mass_multi": mass_m,
|
| 371 |
+
"val_out_multi": val_out_m,
|
| 372 |
+
"val_kl_multi": _s(per_stage_vkl_m),
|
| 373 |
+
"last_ckpt_step": int(last_ckpt_step),
|
| 374 |
+
})
|
| 375 |
+
with tf_summary_writer.as_default():
|
| 376 |
+
tf.summary.scalar("loss", metrics["loss"].mean(), step=step)
|
| 377 |
+
tf.summary.scalar("ce_loss", metrics["ce_loss"].mean(), step=step)
|
| 378 |
+
tf.summary.scalar("aux_bce_loss", metrics["aux_loss"].mean(), step=step)
|
| 379 |
+
tf.summary.scalar(
|
| 380 |
+
"learning rate", metrics["learning_rate"].mean(), step=step
|
| 381 |
+
)
|
| 382 |
+
tf.summary.scalar("curriculum_stage", curriculum.stage, step=step)
|
| 383 |
+
|
| 384 |
+
log_dict = {'loss': metrics["loss"].mean(), 'learning rate': metrics["learning_rate"].mean()}
|
| 385 |
+
|
| 386 |
+
for key in eval_metrics.keys():
|
| 387 |
+
vals = eval_metrics[key]
|
| 388 |
+
if not vals:
|
| 389 |
+
continue
|
| 390 |
+
tf.summary.scalar(
|
| 391 |
+
"eval_" + key, np.array(vals).mean(), step=step
|
| 392 |
+
)
|
| 393 |
+
log_dict[ "eval_" + key ] = np.array(vals).mean()
|
| 394 |
+
|
| 395 |
+
for lvl, v in per_level.items():
|
| 396 |
+
if v >= 0:
|
| 397 |
+
tf.summary.scalar(f"eval_acc_level_{lvl}", v, step=step)
|
| 398 |
+
log_dict[f"eval_acc_level_{lvl}"] = v
|
| 399 |
+
|
| 400 |
+
for s, v in per_slot.items():
|
| 401 |
+
if v >= 0:
|
| 402 |
+
tf.summary.scalar(f"eval_cand_depth_{s}", v, step=step)
|
| 403 |
+
log_dict[f"eval_cand_depth_{s}"] = v
|
| 404 |
+
for s, v in per_slot_ch.items():
|
| 405 |
+
if v >= 0:
|
| 406 |
+
tf.summary.scalar(f"eval_cand_depth_chg_{s}", v, step=step)
|
| 407 |
+
|
| 408 |
+
if config.use_wandb: wandb.log(log_dict, step=step)
|
| 409 |
+
|
| 410 |
+
# ---- Curriculum promotion (ATC-style: threshold + patience) ----
|
| 411 |
+
# Two curricula, one frontier rule. With latent slots the
|
| 412 |
+
# frontier is the DEEPEST active slot, i.e. the newest wave
|
| 413 |
+
# snapshot this stage introduced, scored on changed cells only
|
| 414 |
+
# (unchanged cells are copies of slot j-1 and stay correct for a
|
| 415 |
+
# head that learned nothing new). With the round-count data
|
| 416 |
+
# curriculum the frontier is the newest round-BIN, i.e. the
|
| 417 |
+
# longest-chain puzzles this stage admitted.
|
| 418 |
+
rounds_curric = str(
|
| 419 |
+
getattr(config, "data_curriculum", "none")) == "rounds"
|
| 420 |
+
has_frontier = int(config.num_latent_slots) > 0 or rounds_curric
|
| 421 |
+
if has_frontier and curriculum.stage < curriculum.max_stage:
|
| 422 |
+
t = curriculum.stage
|
| 423 |
+
loc_now = _m("loc_wave")
|
| 424 |
+
if instance_mode:
|
| 425 |
+
# Frontier = probability mass the value head puts on
|
| 426 |
+
# stage t's candidate set, measured teacher-forced so
|
| 427 |
+
# it does not depend on the model's choice of order.
|
| 428 |
+
# This is the soft form of the old in-set rate, which
|
| 429 |
+
# was conditioned on an exact positional location
|
| 430 |
+
# match and so was estimated from ~4% of steps.
|
| 431 |
+
# Frontier = exp(-val_excess_multi) on stage t, i.e.
|
| 432 |
+
# the superposition score mapped into (0, 1] so the
|
| 433 |
+
# existing ">= threshold" machinery still applies.
|
| 434 |
+
# val_excess_multi = log(1/mass) + KL(Uniform(S)||q)
|
| 435 |
+
# on |S| >= 2 cells, so it reaches 0 (score 1.0)
|
| 436 |
+
# only when the model puts all its mass on the
|
| 437 |
+
# candidate set AND spreads uniformly over it.
|
| 438 |
+
exc_now = per_stage_vexc_m.get(t, -1.0)
|
| 439 |
+
frontier_acc = (float(np.exp(-exc_now))
|
| 440 |
+
if exc_now >= 0 else -1.0)
|
| 441 |
+
elif int(config.num_latent_slots) > 0:
|
| 442 |
+
frontier_acc = per_slot_ch.get(t, -1.0)
|
| 443 |
+
if frontier_acc < 0:
|
| 444 |
+
frontier_acc = per_slot.get(t, -1.0)
|
| 445 |
+
else:
|
| 446 |
+
frontier_acc = per_bin.get(t, -1.0)
|
| 447 |
+
if frontier_acc < 0:
|
| 448 |
+
# The frontier bin holds as little as 6.5% of the
|
| 449 |
+
# corpus, so a single eval can miss it. Fall back to
|
| 450 |
+
# the pooled accuracy over everything unlocked.
|
| 451 |
+
seen = [v for b, v in per_bin.items()
|
| 452 |
+
if b <= t and v >= 0]
|
| 453 |
+
frontier_acc = (float(np.mean(seen)) if seen
|
| 454 |
+
else -1.0)
|
| 455 |
+
steps_in_stage = step - stage_started_at
|
| 456 |
+
# A negative accuracy means "not measured this eval". Never
|
| 457 |
+
# let that count as evidence: it must not reset the plateau
|
| 458 |
+
# tracker, and it must not satisfy the threshold or plateau
|
| 459 |
+
# rule. Only the hard patience cap can fire without a
|
| 460 |
+
# measurement.
|
| 461 |
+
measured = frontier_acc >= 0
|
| 462 |
+
if measured and frontier_acc > stage_best_acc + plateau_delta:
|
| 463 |
+
stage_best_acc = frontier_acc
|
| 464 |
+
stage_best_step = step
|
| 465 |
+
# Location gate. The upstream code never compared the
|
| 466 |
+
# emitted (r,c) to the target order at all, and the
|
| 467 |
+
# earlier gate on the positional loc_acc was
|
| 468 |
+
# unsatisfiable (0.70 required, ~0.04 observed), so every
|
| 469 |
+
# promotion fired on the patience cap alone. It now keys
|
| 470 |
+
# on loc_wave, which credits any cell from the earliest
|
| 471 |
+
# unfilled propagation wave, and defaults to off until
|
| 472 |
+
# the reachable range is known from a run.
|
| 473 |
+
loc_ready = (promote_loc_threshold <= 0.0
|
| 474 |
+
or loc_now >= promote_loc_threshold)
|
| 475 |
+
excess_now = per_stage_vexcess.get(t, -1.0)
|
| 476 |
+
excess_ready = (promote_excess_threshold <= 0.0
|
| 477 |
+
or (0.0 <= excess_now
|
| 478 |
+
<= promote_excess_threshold))
|
| 479 |
+
gates_ok = loc_ready and excess_ready
|
| 480 |
+
hit_threshold = (measured and gates_ok
|
| 481 |
+
and frontier_acc >= promote_threshold)
|
| 482 |
+
stalled = (measured and gates_ok and plateau_steps > 0
|
| 483 |
+
and (step - stage_best_step) >= plateau_steps)
|
| 484 |
+
patience_over = steps_in_stage >= promote_patience
|
| 485 |
+
if steps_in_stage >= min_stage_steps and (
|
| 486 |
+
hit_threshold or stalled or patience_over):
|
| 487 |
+
reason = ("threshold" if hit_threshold
|
| 488 |
+
else "plateau" if stalled else "patience")
|
| 489 |
+
curriculum.stage += 1
|
| 490 |
+
stage_started_at = step
|
| 491 |
+
stage_best_acc = -1.0
|
| 492 |
+
stage_best_step = step
|
| 493 |
+
p_train_step = make_p_train_step(passes_for(curriculum.stage))
|
| 494 |
+
what = ("round-bin" if int(config.num_latent_slots) == 0
|
| 495 |
+
else "depth")
|
| 496 |
+
print(f"[curriculum] step {step}: promote to stage "
|
| 497 |
+
f"{curriculum.stage} ({reason}; graduated {what} "
|
| 498 |
+
f"{t} acc={frontier_acc:.3f} after "
|
| 499 |
+
f"{steps_in_stage} steps); "
|
| 500 |
+
f"latent passes={curriculum.stage}, "
|
| 501 |
+
f"pool/snapshots now 1..{curriculum.stage}",
|
| 502 |
+
flush=True)
|
| 503 |
+
if config.save_checkpoint:
|
| 504 |
+
unrep_state = jax_utils.unreplicate(state)
|
| 505 |
+
# Rolling checkpoint in the main workdir.
|
| 506 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 507 |
+
workdir, unrep_state, step,
|
| 508 |
+
keep=ckpt_keep, overwrite=True)
|
| 509 |
+
# Protected copy: the model *entering* this stage,
|
| 510 |
+
# kept permanently under stage_ckpts/ (never rolled
|
| 511 |
+
# off), so every stage's checkpoint survives.
|
| 512 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 513 |
+
stage_ckpt_dir, unrep_state, step,
|
| 514 |
+
keep=100, overwrite=True,
|
| 515 |
+
prefix=f"stage{curriculum.stage}_")
|
| 516 |
+
last_ckpt_step = step
|
| 517 |
+
_write_json(os.path.join(workdir, "ckpt_ready.json"), {
|
| 518 |
+
"event": "stage",
|
| 519 |
+
"step": int(step),
|
| 520 |
+
"stage": int(curriculum.stage),
|
| 521 |
+
"reason": reason,
|
| 522 |
+
"workdir": workdir,
|
| 523 |
+
"stage_ckpt_dir": stage_ckpt_dir,
|
| 524 |
+
"keep_snapshot": True,
|
| 525 |
+
})
|
| 526 |
+
_write_heartbeat(workdir, {
|
| 527 |
+
"event": "promote",
|
| 528 |
+
"step": int(step),
|
| 529 |
+
"stage": int(curriculum.stage),
|
| 530 |
+
"reason": reason,
|
| 531 |
+
"graduated_acc": round(float(frontier_acc), 6),
|
| 532 |
+
"last_ckpt_step": int(last_ckpt_step),
|
| 533 |
+
})
|
| 534 |
+
|
| 535 |
+
if config.save_checkpoint and step > 0 and step % config.save_every_steps == 0:
|
| 536 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 537 |
+
workdir, jax_utils.unreplicate(state), step,
|
| 538 |
+
keep=ckpt_keep, overwrite=True
|
| 539 |
+
)
|
| 540 |
+
last_ckpt_step = step
|
| 541 |
+
_write_json(os.path.join(workdir, "ckpt_ready.json"), {
|
| 542 |
+
"event": "periodic",
|
| 543 |
+
"step": int(step),
|
| 544 |
+
"stage": int(curriculum.stage),
|
| 545 |
+
"workdir": workdir,
|
| 546 |
+
"keep_snapshot": (step % 50000 == 0),
|
| 547 |
+
})
|
| 548 |
+
_write_heartbeat(workdir, {
|
| 549 |
+
"event": "ckpt",
|
| 550 |
+
"step": int(step),
|
| 551 |
+
"stage": int(curriculum.stage),
|
| 552 |
+
"last_ckpt_step": int(last_ckpt_step),
|
| 553 |
+
})
|
| 554 |
+
|
| 555 |
+
if stop_requested["flag"]:
|
| 556 |
+
print(f"[signal] stopping at step {step}", flush=True)
|
| 557 |
+
if config.save_checkpoint:
|
| 558 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 559 |
+
workdir, jax_utils.unreplicate(state), step,
|
| 560 |
+
keep=ckpt_keep, overwrite=True)
|
| 561 |
+
last_ckpt_step = step
|
| 562 |
+
_write_json(os.path.join(workdir, "ckpt_ready.json"), {
|
| 563 |
+
"event": "signal",
|
| 564 |
+
"step": int(step),
|
| 565 |
+
"stage": int(curriculum.stage),
|
| 566 |
+
"workdir": workdir,
|
| 567 |
+
"keep_snapshot": True,
|
| 568 |
+
})
|
| 569 |
+
_write_heartbeat(workdir, {
|
| 570 |
+
"event": "stopped",
|
| 571 |
+
"step": int(step),
|
| 572 |
+
"stage": int(curriculum.stage),
|
| 573 |
+
"last_ckpt_step": int(last_ckpt_step),
|
| 574 |
+
})
|
| 575 |
+
break
|
| 576 |
+
|
| 577 |
+
# Final checkpoint at the end of training
|
| 578 |
+
if config.save_checkpoint and not stop_requested["flag"]:
|
| 579 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 580 |
+
workdir, jax_utils.unreplicate(state), config.max_steps,
|
| 581 |
+
keep=ckpt_keep, overwrite=True)
|
| 582 |
+
_write_json(os.path.join(workdir, "ckpt_ready.json"), {
|
| 583 |
+
"event": "final",
|
| 584 |
+
"step": int(config.max_steps),
|
| 585 |
+
"stage": int(curriculum.stage),
|
| 586 |
+
"workdir": workdir,
|
| 587 |
+
"keep_snapshot": True,
|
| 588 |
+
})
|
| 589 |
+
_write_heartbeat(workdir, {
|
| 590 |
+
"event": "done",
|
| 591 |
+
"step": int(last_ckpt_step),
|
| 592 |
+
"stage": int(curriculum.stage),
|
| 593 |
+
"last_ckpt_step": int(last_ckpt_step),
|
| 594 |
+
})
|
| 595 |
+
|
| 596 |
+
|
code/wavecurriculum_run/train/train_backtrack.py
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backtracking (stage-replay) training loop for the recurrent latent model.
|
| 2 |
+
|
| 3 |
+
Motivation
|
| 4 |
+
----------
|
| 5 |
+
Standard curriculum training runs EVERY batch at `num_passes = curriculum.stage`
|
| 6 |
+
(the current frontier depth). When the curriculum advances to stage i, the
|
| 7 |
+
earlier, shallower readouts (stage 1..i-1, each of which should be produced by
|
| 8 |
+
applying the tied recurrent step exactly t times) drift / are forgotten.
|
| 9 |
+
|
| 10 |
+
Backtracking = interleaved replay with the recurrence depth MATCHED to the
|
| 11 |
+
replayed stage:
|
| 12 |
+
|
| 13 |
+
stage t <-> difficulty level t+2 <-> num_passes = t <-> t active slots
|
| 14 |
+
|
| 15 |
+
With cand_slot_mode="depth" and passes_per_stage=pps the depth becomes pps*t and
|
| 16 |
+
the active-slot count follows num_passes rather than the puzzle level. Replay is
|
| 17 |
+
still safe because that mode maps slot j -> stage j: slots [0, pps*t) hold the
|
| 18 |
+
right targets no matter which frontier stage the sampler built the batch for.
|
| 19 |
+
|
| 20 |
+
* frontier step : level (i+2) puzzles, num_passes = i -> trains stage-i readout
|
| 21 |
+
* backtrack step: level (t+2) puzzles, num_passes = t -> re-derives stage-t
|
| 22 |
+
for a randomly chosen earlier t in {1..i-1}
|
| 23 |
+
|
| 24 |
+
Each optimisation step is frontier with prob (1 - backtrack_prob) and a backtrack
|
| 25 |
+
step with prob backtrack_prob (t uniform over the earlier stages). Because the
|
| 26 |
+
recurrent step is weight-tied, replaying "apply f exactly t times = stage t"
|
| 27 |
+
keeps f reusable at every depth instead of specialising to the frontier depth.
|
| 28 |
+
|
| 29 |
+
Everything else (model, data, per-slot BCE + LM CE loss, eval, promotion) is
|
| 30 |
+
reused unchanged from the standard pipeline.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
import functools
|
| 34 |
+
import math
|
| 35 |
+
import os
|
| 36 |
+
|
| 37 |
+
from absl import logging
|
| 38 |
+
from clu import metric_writers
|
| 39 |
+
from flax import jax_utils
|
| 40 |
+
from flax.training import checkpoints
|
| 41 |
+
from flax.training import common_utils
|
| 42 |
+
import jax
|
| 43 |
+
import numpy as np
|
| 44 |
+
import tensorflow as tf
|
| 45 |
+
|
| 46 |
+
from train import data
|
| 47 |
+
from train import evaluater
|
| 48 |
+
from train import model
|
| 49 |
+
from train import trainer
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class StageBatchSampler:
|
| 53 |
+
"""Draws minibatches of a *specific* difficulty level from the train set.
|
| 54 |
+
|
| 55 |
+
Reuses SudokuDataset (which loads the puzzles + staged candidate masks and
|
| 56 |
+
builds a level->indices map) and assembles numpy batches in the exact tuple
|
| 57 |
+
layout the train step expects: (seq, puzzle, start_index, level, cand_targets).
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
def __init__(self, config, seed=0):
|
| 61 |
+
self.ds = data.SudokuDataset(config, train=True)
|
| 62 |
+
self.rng = np.random.RandomState(seed)
|
| 63 |
+
# Only levels that actually have puzzles.
|
| 64 |
+
self.available = {lvl: idx for lvl, idx in self.ds.level_index.items()
|
| 65 |
+
if len(idx) > 0}
|
| 66 |
+
|
| 67 |
+
def has_level(self, level):
|
| 68 |
+
return level in self.available
|
| 69 |
+
|
| 70 |
+
def _gather(self, idxs):
|
| 71 |
+
seqs, puzzles, starts, levels, cands = [], [], [], [], []
|
| 72 |
+
for idx in idxs:
|
| 73 |
+
seq, puzzle, start_index, lvl, cand = self.ds.__getitem__(int(idx))
|
| 74 |
+
seqs.append(seq)
|
| 75 |
+
puzzles.append(puzzle)
|
| 76 |
+
starts.append(start_index)
|
| 77 |
+
levels.append(lvl)
|
| 78 |
+
cands.append(cand)
|
| 79 |
+
return (
|
| 80 |
+
np.stack(seqs).astype(np.int32),
|
| 81 |
+
np.stack(puzzles).astype(np.int32),
|
| 82 |
+
np.stack(starts).astype(np.int32),
|
| 83 |
+
np.stack(levels).astype(np.int32),
|
| 84 |
+
np.stack(cands).astype(np.int32),
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
def sample_all(self, bs):
|
| 88 |
+
"""Batch drawn uniformly from the whole corpus, matching the standard
|
| 89 |
+
loop's sampler. Difficulty is not a curriculum axis, so every step --
|
| 90 |
+
frontier or repair -- uses this same distribution and differs only in
|
| 91 |
+
the recurrence depth it trains at."""
|
| 92 |
+
return self._gather(self.rng.randint(len(self.ds), size=bs))
|
| 93 |
+
|
| 94 |
+
def sample(self, level, bs):
|
| 95 |
+
idx_pool = self.available[level]
|
| 96 |
+
idxs = idx_pool[self.rng.randint(len(idx_pool), size=bs)]
|
| 97 |
+
return self._gather(idxs)
|
| 98 |
+
|
| 99 |
+
def sample_mixed(self, levels, bs):
|
| 100 |
+
"""Batch drawn uniformly over `levels` (level first, then a puzzle of
|
| 101 |
+
that level) — the exact distribution the standard curriculum sampler
|
| 102 |
+
uses. Frontier steps must use this, NOT single-level batches: training
|
| 103 |
+
exclusively on the frontier level starves every easier level and is a
|
| 104 |
+
different (worse) recipe than the standard loop, not "standard + BT"."""
|
| 105 |
+
levels = [l for l in levels if l in self.available]
|
| 106 |
+
idxs = []
|
| 107 |
+
for _ in range(bs):
|
| 108 |
+
lvl = levels[self.rng.randint(len(levels))]
|
| 109 |
+
pool = self.available[lvl]
|
| 110 |
+
idxs.append(int(pool[self.rng.randint(len(pool))]))
|
| 111 |
+
return self._gather(idxs)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _run_train_step(step_fn, state, batch_tuple, dropout_rngs):
|
| 115 |
+
"""Shard a single numpy batch and run one (pmapped) train step."""
|
| 116 |
+
inputs, _, start_index, levels, cand_targets = batch_tuple
|
| 117 |
+
inputs = common_utils.shard(jax.tree_util.tree_map(np.asarray, inputs))
|
| 118 |
+
start_index = common_utils.shard(jax.tree_util.tree_map(np.asarray, start_index))
|
| 119 |
+
levels = common_utils.shard(jax.tree_util.tree_map(np.asarray, levels))
|
| 120 |
+
cand_targets = np.asarray(cand_targets)
|
| 121 |
+
_nd = jax.local_device_count()
|
| 122 |
+
cand_targets = cand_targets.reshape(
|
| 123 |
+
(_nd, cand_targets.shape[0] // _nd) + cand_targets.shape[1:])
|
| 124 |
+
state, metrics, _ = step_fn(
|
| 125 |
+
state, inputs, start_index, levels, cand_targets, dropout_rng=dropout_rngs)
|
| 126 |
+
return state, metrics
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _prepare_backtrack(config, workdir):
|
| 130 |
+
"""Build the model, state, pmapped steps, sampler, and writers shared by the
|
| 131 |
+
probabilistic and adaptive backtracking loops.
|
| 132 |
+
|
| 133 |
+
Returns a dict of everything both loops need. `state` is already replicated.
|
| 134 |
+
"""
|
| 135 |
+
K = int(config.num_latent_slots)
|
| 136 |
+
assert K > 0, "Backtracking is for the recurrent latent model (K>0)."
|
| 137 |
+
|
| 138 |
+
logging.info("Creating datasets (backtracking loop)")
|
| 139 |
+
curriculum = data.CurriculumState(
|
| 140 |
+
stage=int(getattr(config, "curriculum_start_stage", 1)),
|
| 141 |
+
max_stage=int(getattr(config, "curriculum_max_stage", 6)))
|
| 142 |
+
# Per-level train sampler (replaces the uniform-over-unlocked sampler so we
|
| 143 |
+
# can draw a batch of a specific replay stage's level on demand).
|
| 144 |
+
sampler = StageBatchSampler(config, seed=int(config.seed))
|
| 145 |
+
eval_data_iter = data.create_iter(config, config.minibatch_size, train=False)
|
| 146 |
+
|
| 147 |
+
model_config = model.TransformerConfig(
|
| 148 |
+
dtype=config.dtype, vocab_size=config.vocab_size, seq_len=config.seq_len,
|
| 149 |
+
num_heads=config.num_heads, num_layers=config.num_layers,
|
| 150 |
+
emb_dim=config.emb_dim, qkv_dim=config.qkv_dim, mlp_dim=config.mlp_dim,
|
| 151 |
+
dropout_rate=config.dropout_rate,
|
| 152 |
+
attention_dropout_rate=config.attention_dropout_rate,
|
| 153 |
+
deterministic=False, num_latent_slots=K,
|
| 154 |
+
inject_latents=bool(int(getattr(config, "recurrent_latent", 1))),
|
| 155 |
+
)
|
| 156 |
+
print(str(model_config.__dict__), flush=True)
|
| 157 |
+
|
| 158 |
+
rng = jax.random.PRNGKey(config.seed)
|
| 159 |
+
rng, init_rng, dropout_rng = jax.random.split(rng, 3)
|
| 160 |
+
net = model.TransformerLMHeadModel(model_config)
|
| 161 |
+
dummy_latents = jax.numpy.zeros(
|
| 162 |
+
(config.minibatch_size, K, config.emb_dim), model_config.dtype)
|
| 163 |
+
dummy_positions = jax.numpy.zeros((config.minibatch_size, K), jax.numpy.int32)
|
| 164 |
+
dummy_active = jax.numpy.zeros((config.minibatch_size, K), bool)
|
| 165 |
+
_, initial_variables = jax.jit(net.init_with_output)(
|
| 166 |
+
{"params": init_rng, "dropout": dropout_rng},
|
| 167 |
+
jax.numpy.ones((config.minibatch_size, config.seq_len), jax.numpy.int32),
|
| 168 |
+
dummy_latents, dummy_positions, dummy_active)
|
| 169 |
+
|
| 170 |
+
state, lr_scheduler_fn = trainer.get_state(config, net, initial_variables)
|
| 171 |
+
start_step = 0
|
| 172 |
+
if config.resume_training:
|
| 173 |
+
state = checkpoints.restore_checkpoint(config.ckpt_loc, state)
|
| 174 |
+
start_step = int(state.step)
|
| 175 |
+
print("----------Restored model from", config.ckpt_loc,
|
| 176 |
+
f"at step {start_step}-----------")
|
| 177 |
+
|
| 178 |
+
writer = metric_writers.create_default_writer(
|
| 179 |
+
workdir, asynchronous=False, just_logging=(jax.process_index() > 0))
|
| 180 |
+
tf_summary_writer = tf.summary.create_file_writer(workdir)
|
| 181 |
+
|
| 182 |
+
state = jax_utils.replicate(state)
|
| 183 |
+
dropout_rngs = jax.random.split(rng, jax.local_device_count())
|
| 184 |
+
|
| 185 |
+
def make_p_train_step(num_passes, backtrack):
|
| 186 |
+
return jax.pmap(
|
| 187 |
+
functools.partial(
|
| 188 |
+
trainer.train_step, config=model_config, hyperparams=config,
|
| 189 |
+
learning_rate_fn=lr_scheduler_fn, num_passes=num_passes,
|
| 190 |
+
backtrack=backtrack),
|
| 191 |
+
axis_name="batch", donate_argnums=(0,))
|
| 192 |
+
|
| 193 |
+
# Precompile, for every recurrence depth 1..K, both a FRONTIER step (normal
|
| 194 |
+
# loss: LM CE + all active-slot BCE) and a strict BACKTRACK step (only the
|
| 195 |
+
# last active slot's readout, no CE). Frontier is used when t == curriculum
|
| 196 |
+
# stage i; the strict backtrack step is used by the probabilistic loop for
|
| 197 |
+
# replayed earlier stages t < i.
|
| 198 |
+
# Stage -> recurrence depth. With passes_per_stage>1 a stage advances the
|
| 199 |
+
# chain by more than one slot (pps=2, K=12: stage 6 -> all 12 slots), so the
|
| 200 |
+
# replay depth for stage t is pps*t, not t. Keyed by stage either way.
|
| 201 |
+
pps = int(getattr(config, "passes_per_stage", 1))
|
| 202 |
+
_depth = lambda t: min(pps * t, K)
|
| 203 |
+
p_frontier = {t: make_p_train_step(_depth(t), False) for t in range(1, K + 1)}
|
| 204 |
+
p_backtrack = {t: make_p_train_step(_depth(t), True) for t in range(1, K + 1)}
|
| 205 |
+
p_eval_step = jax.pmap(functools.partial(
|
| 206 |
+
evaluater.eval_step, config=model_config.replace(deterministic=True)),
|
| 207 |
+
axis_name="batch")
|
| 208 |
+
|
| 209 |
+
hooks, report_progress, _ = trainer.get_metrics_report_progress(
|
| 210 |
+
config, workdir, writer)
|
| 211 |
+
|
| 212 |
+
return {
|
| 213 |
+
"K": K,
|
| 214 |
+
"start_step": start_step,
|
| 215 |
+
"curriculum": curriculum,
|
| 216 |
+
"sampler": sampler,
|
| 217 |
+
"eval_data_iter": eval_data_iter,
|
| 218 |
+
"model_config": model_config,
|
| 219 |
+
"state": state,
|
| 220 |
+
"dropout_rngs": dropout_rngs,
|
| 221 |
+
"p_frontier": p_frontier,
|
| 222 |
+
"p_backtrack": p_backtrack,
|
| 223 |
+
"p_eval_step": p_eval_step,
|
| 224 |
+
"hooks": hooks,
|
| 225 |
+
"report_progress": report_progress,
|
| 226 |
+
"writer": writer,
|
| 227 |
+
"tf_summary_writer": tf_summary_writer,
|
| 228 |
+
"promote_threshold": float(getattr(config, "promote_acc_threshold", 0.85)),
|
| 229 |
+
"promote_patience": int(getattr(config, "promote_patience_steps", 8000)),
|
| 230 |
+
"min_stage_steps": int(getattr(config, "min_stage_steps", 2000)),
|
| 231 |
+
"ckpt_keep": int(getattr(config, "ckpt_keep", 100)),
|
| 232 |
+
"stage_ckpt_dir": os.path.join(workdir, "stage_ckpts"),
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def train_and_evaluate_backtrack(config, workdir):
|
| 237 |
+
"""Backtracking curriculum training loop (stage replay with matched depth)."""
|
| 238 |
+
workdir = os.path.abspath(workdir)
|
| 239 |
+
backtrack_prob = float(getattr(config, "backtrack_prob", 0.5))
|
| 240 |
+
frontier_mix = bool(int(getattr(config, "backtrack_frontier_mix", 1)))
|
| 241 |
+
|
| 242 |
+
ctx = _prepare_backtrack(config, workdir)
|
| 243 |
+
K = ctx["K"]
|
| 244 |
+
curriculum = ctx["curriculum"]
|
| 245 |
+
sampler = ctx["sampler"]
|
| 246 |
+
eval_data_iter = ctx["eval_data_iter"]
|
| 247 |
+
state = ctx["state"]
|
| 248 |
+
dropout_rngs = ctx["dropout_rngs"]
|
| 249 |
+
p_frontier = ctx["p_frontier"]
|
| 250 |
+
p_backtrack = ctx["p_backtrack"]
|
| 251 |
+
p_eval_step = ctx["p_eval_step"]
|
| 252 |
+
hooks = ctx["hooks"]
|
| 253 |
+
writer = ctx["writer"]
|
| 254 |
+
tf_summary_writer = ctx["tf_summary_writer"]
|
| 255 |
+
promote_threshold = ctx["promote_threshold"]
|
| 256 |
+
promote_patience = ctx["promote_patience"]
|
| 257 |
+
min_stage_steps = ctx["min_stage_steps"]
|
| 258 |
+
ckpt_keep = ctx["ckpt_keep"]
|
| 259 |
+
stage_ckpt_dir = ctx["stage_ckpt_dir"]
|
| 260 |
+
stage_started_at = 0
|
| 261 |
+
sched_rng = np.random.RandomState(int(config.seed) + 1)
|
| 262 |
+
|
| 263 |
+
# Bookkeeping: how often each replay depth is actually trained.
|
| 264 |
+
stage_step_counts = {t: 0 for t in range(1, K + 1)}
|
| 265 |
+
|
| 266 |
+
def sample_target_stage(i):
|
| 267 |
+
"""Frontier stage i with prob (1-p); else a backtrack stage 1..i-1."""
|
| 268 |
+
if i <= 1 or sched_rng.rand() >= backtrack_prob:
|
| 269 |
+
return i
|
| 270 |
+
return int(sched_rng.randint(1, i)) # uniform in {1..i-1}
|
| 271 |
+
|
| 272 |
+
with metric_writers.ensure_flushes(writer):
|
| 273 |
+
for step in range(0, config.max_steps):
|
| 274 |
+
if step % 10000 == 0:
|
| 275 |
+
print("Step:", step, flush=True)
|
| 276 |
+
|
| 277 |
+
i = curriculum.stage
|
| 278 |
+
t = sample_target_stage(i)
|
| 279 |
+
# Frontier step (t == i): normal loss. Backtrack step (t < i): strict
|
| 280 |
+
# replay of the depth-t readout only. Both draw from the whole
|
| 281 |
+
# corpus: depth, not difficulty, is the replayed axis.
|
| 282 |
+
step_fn = p_frontier[t] if t == i else p_backtrack[t]
|
| 283 |
+
batch = sampler.sample_all(config.minibatch_size)
|
| 284 |
+
state, metrics = _run_train_step(step_fn, state, batch, dropout_rngs)
|
| 285 |
+
stage_step_counts[t] += 1
|
| 286 |
+
|
| 287 |
+
for h in hooks:
|
| 288 |
+
h(step)
|
| 289 |
+
|
| 290 |
+
if math.isnan(metrics["loss"][0]):
|
| 291 |
+
print("Loss became nan; stopping.", flush=True)
|
| 292 |
+
break
|
| 293 |
+
|
| 294 |
+
if step % config.eval_every_steps == 0:
|
| 295 |
+
eval_metrics = evaluater.get_eval_metrics(
|
| 296 |
+
state, eval_data_iter, p_eval_step, config)
|
| 297 |
+
per_level = eval_metrics.pop("per_level_acc")
|
| 298 |
+
per_depth = eval_metrics.pop("per_slot_acc_changed", {})
|
| 299 |
+
per_depth_all = eval_metrics.pop("per_slot_acc", {})
|
| 300 |
+
if not any(v >= 0 for v in per_depth.values()):
|
| 301 |
+
per_depth = per_depth_all
|
| 302 |
+
|
| 303 |
+
def _m(key):
|
| 304 |
+
v = eval_metrics.get(key, [])
|
| 305 |
+
return round(float(np.mean(v)), 4) if len(v) else -1.0
|
| 306 |
+
|
| 307 |
+
print(step, "stage", curriculum.stage,
|
| 308 |
+
"target_t", t,
|
| 309 |
+
"loss", round(float(metrics["loss"].mean()), 4),
|
| 310 |
+
"ce", round(float(metrics["ce_loss"].mean()), 4),
|
| 311 |
+
"aux_bce", round(float(metrics["aux_loss"].mean()), 4),
|
| 312 |
+
"| val_acc", _m("acc"), "loc_acc", _m("loc_acc"),
|
| 313 |
+
"val|loc", _m("val_given_loc_acc"),
|
| 314 |
+
"| cand_bit_acc", _m("cand_bit_acc"),
|
| 315 |
+
"cand_set_acc", _m("cand_set_acc"),
|
| 316 |
+
"cand_set_chg", _m("cand_set_acc_changed"),
|
| 317 |
+
"| replay_counts", dict(stage_step_counts),
|
| 318 |
+
flush=True)
|
| 319 |
+
|
| 320 |
+
with tf_summary_writer.as_default():
|
| 321 |
+
tf.summary.scalar("loss", metrics["loss"].mean(), step=step)
|
| 322 |
+
tf.summary.scalar("ce_loss", metrics["ce_loss"].mean(), step=step)
|
| 323 |
+
tf.summary.scalar("aux_bce_loss", metrics["aux_loss"].mean(), step=step)
|
| 324 |
+
tf.summary.scalar("curriculum_stage", curriculum.stage, step=step)
|
| 325 |
+
for key in eval_metrics.keys():
|
| 326 |
+
tf.summary.scalar("eval_" + key,
|
| 327 |
+
np.array(eval_metrics[key]).mean(), step=step)
|
| 328 |
+
for lvl, v in per_level.items():
|
| 329 |
+
if v >= 0:
|
| 330 |
+
tf.summary.scalar(f"eval_acc_level_{lvl}", v, step=step)
|
| 331 |
+
|
| 332 |
+
# ---- Curriculum promotion (same rule as the standard loop) ----
|
| 333 |
+
if curriculum.stage < curriculum.max_stage:
|
| 334 |
+
frontier_acc = per_depth.get(curriculum.stage, -1.0)
|
| 335 |
+
steps_in_stage = step - stage_started_at
|
| 336 |
+
hit_threshold = frontier_acc >= promote_threshold
|
| 337 |
+
patience_over = steps_in_stage >= promote_patience
|
| 338 |
+
if steps_in_stage >= min_stage_steps and (hit_threshold or patience_over):
|
| 339 |
+
reason = "threshold" if hit_threshold else "patience"
|
| 340 |
+
curriculum.stage += 1
|
| 341 |
+
stage_started_at = step
|
| 342 |
+
print(f"[curriculum] step {step}: promote to stage "
|
| 343 |
+
f"{curriculum.stage} ({reason}; graduated depth "
|
| 344 |
+
f"{curriculum.stage - 1} cand-set "
|
| 345 |
+
f"acc={frontier_acc:.3f}); "
|
| 346 |
+
f"backtrack pool now depths 1..{curriculum.stage-1}",
|
| 347 |
+
flush=True)
|
| 348 |
+
if config.save_checkpoint:
|
| 349 |
+
unrep = jax_utils.unreplicate(state)
|
| 350 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 351 |
+
workdir, unrep, step, keep=ckpt_keep, overwrite=True)
|
| 352 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 353 |
+
stage_ckpt_dir, unrep, step, keep=100,
|
| 354 |
+
overwrite=True, prefix=f"stage{curriculum.stage}_")
|
| 355 |
+
|
| 356 |
+
if config.save_checkpoint and step > 0 and step % config.save_every_steps == 0:
|
| 357 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 358 |
+
workdir, jax_utils.unreplicate(state), step,
|
| 359 |
+
keep=ckpt_keep, overwrite=True)
|
| 360 |
+
|
| 361 |
+
if config.save_checkpoint:
|
| 362 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 363 |
+
workdir, jax_utils.unreplicate(state), config.max_steps,
|
| 364 |
+
keep=ckpt_keep, overwrite=True)
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def train_and_evaluate_backtrack_adaptive(config, workdir):
|
| 368 |
+
"""Deficit-driven ("adaptive") backtracking loop.
|
| 369 |
+
|
| 370 |
+
Difference from the probabilistic loop
|
| 371 |
+
--------------------------------------
|
| 372 |
+
The probabilistic loop replays a *uniformly random* earlier stage every step
|
| 373 |
+
with fixed probability p, regardless of whether that stage needs help. This
|
| 374 |
+
loop instead *watches* each earlier stage's held-out accuracy and only goes
|
| 375 |
+
back to repair a stage when it has actually regressed:
|
| 376 |
+
|
| 377 |
+
* Reference: when the curriculum promotes past stage t, we record that
|
| 378 |
+
stage's frontier-level (t+2) val_acc as its "graduation" accuracy.
|
| 379 |
+
* Trigger (relative drop): while at frontier stage i, at every eval we scan
|
| 380 |
+
earlier stages 1..i-1; a stage t is *in deficit* if its current level-(t+2)
|
| 381 |
+
val_acc has fallen below (graduation_acc[t] - margin).
|
| 382 |
+
* Selection (most-deficient first): if any stage is in deficit we switch the
|
| 383 |
+
training target to the single most-regressed stage and train there.
|
| 384 |
+
* Repair step: a FULL frontier-style step at the matched depth (level t+2,
|
| 385 |
+
num_passes=t, LM CE + all-active-slot BCE) -- NOT the strict readout-only
|
| 386 |
+
backtrack step. The metric we are trying to restore is placement accuracy
|
| 387 |
+
(driven by the LM CE), so the repair objective must include it.
|
| 388 |
+
* Exit (recover-or-cap): stay on the stage until its val_acc climbs back
|
| 389 |
+
above (graduation_acc[t] - margin), or until a max-repair-steps cap fires
|
| 390 |
+
(so a stuck stage cannot stall the frontier forever). On exit we re-scan
|
| 391 |
+
and either move to the next most-deficient stage or return to the frontier.
|
| 392 |
+
|
| 393 |
+
Frontier promotion is paused while repairing, and the repair time is credited
|
| 394 |
+
back to the frontier stage's patience clock on return.
|
| 395 |
+
|
| 396 |
+
Depth-matched eval note: the evaluator scores a level-L puzzle with exactly
|
| 397 |
+
k=L-2 active latent slots, so per_level_acc[t+2] is a faithful measurement of
|
| 398 |
+
the stage-t readout -- a clean trigger signal with no extra instrumentation.
|
| 399 |
+
"""
|
| 400 |
+
workdir = os.path.abspath(workdir)
|
| 401 |
+
margin = float(getattr(config, "backtrack_margin", 0.03))
|
| 402 |
+
max_repair_steps = int(getattr(config, "backtrack_max_repair_steps", 6000))
|
| 403 |
+
min_frontier_steps = int(getattr(config, "backtrack_min_frontier_steps", 0))
|
| 404 |
+
frontier_target_acc = float(
|
| 405 |
+
getattr(config, "backtrack_frontier_target_acc", 0.0))
|
| 406 |
+
grad_acc_seed_raw = str(getattr(config, "backtrack_grad_acc_seed", "") or "")
|
| 407 |
+
max_repair_fraction = float(
|
| 408 |
+
getattr(config, "backtrack_max_repair_fraction", 0.0))
|
| 409 |
+
grad_decay = float(getattr(config, "backtrack_grad_decay", 0.0))
|
| 410 |
+
freeze_after_step = int(getattr(config, "backtrack_freeze_after_step", 0))
|
| 411 |
+
frontier_mix = bool(int(getattr(config, "backtrack_frontier_mix", 1)))
|
| 412 |
+
|
| 413 |
+
ctx = _prepare_backtrack(config, workdir)
|
| 414 |
+
K = ctx["K"]
|
| 415 |
+
start_step = ctx["start_step"]
|
| 416 |
+
curriculum = ctx["curriculum"]
|
| 417 |
+
sampler = ctx["sampler"]
|
| 418 |
+
eval_data_iter = ctx["eval_data_iter"]
|
| 419 |
+
state = ctx["state"]
|
| 420 |
+
dropout_rngs = ctx["dropout_rngs"]
|
| 421 |
+
p_frontier = ctx["p_frontier"]
|
| 422 |
+
p_eval_step = ctx["p_eval_step"]
|
| 423 |
+
hooks = ctx["hooks"]
|
| 424 |
+
writer = ctx["writer"]
|
| 425 |
+
tf_summary_writer = ctx["tf_summary_writer"]
|
| 426 |
+
promote_threshold = ctx["promote_threshold"]
|
| 427 |
+
promote_patience = ctx["promote_patience"]
|
| 428 |
+
min_stage_steps = ctx["min_stage_steps"]
|
| 429 |
+
ckpt_keep = ctx["ckpt_keep"]
|
| 430 |
+
stage_ckpt_dir = ctx["stage_ckpt_dir"]
|
| 431 |
+
|
| 432 |
+
# Controller state.
|
| 433 |
+
grad_acc = {} # stage t -> level-(t+2) val_acc at graduation
|
| 434 |
+
plateau_steps = int(getattr(config, "plateau_steps", 0))
|
| 435 |
+
plateau_delta = float(getattr(config, "plateau_delta", 0.005))
|
| 436 |
+
stage_best_acc = -1.0
|
| 437 |
+
stage_best_step = start_step
|
| 438 |
+
mode = "frontier" # "frontier" | "repair"
|
| 439 |
+
repair_stage = None # stage currently being repaired
|
| 440 |
+
repair_started_at = 0 # step the current repair stage began (cap)
|
| 441 |
+
repair_episode_start = 0 # step we first left the frontier (clock credit)
|
| 442 |
+
stage_started_at = start_step # step the current frontier stage began
|
| 443 |
+
last_repair_return_step = start_step # for min-frontier cooldown
|
| 444 |
+
step_counts = {t: 0 for t in range(1, K + 1)} # steps trained at each depth
|
| 445 |
+
repair_steps_total = 0 # for duty-cycle cap
|
| 446 |
+
print(f"[repair] knobs: margin={margin} max_repair_steps={max_repair_steps} "
|
| 447 |
+
f"min_frontier={min_frontier_steps} frontier_target={frontier_target_acc} "
|
| 448 |
+
f"max_repair_frac={max_repair_fraction} grad_decay={grad_decay} "
|
| 449 |
+
f"freeze_after={freeze_after_step} frontier_mix={frontier_mix}",
|
| 450 |
+
flush=True)
|
| 451 |
+
# When seeding from a mid-curriculum checkpoint (start stage > 1), the
|
| 452 |
+
# earlier stages graduated in the *source* run so we have no reference for
|
| 453 |
+
# them. Prefer an explicit SUDOKU_GRAD_ACC_SEED (true graduation refs);
|
| 454 |
+
# otherwise seed from the first eval (old behavior — can over-trigger).
|
| 455 |
+
seed_start_stage = int(getattr(config, "curriculum_start_stage", 1))
|
| 456 |
+
grad_acc_seeded = seed_start_stage <= 1
|
| 457 |
+
if (not grad_acc_seeded) and grad_acc_seed_raw.strip():
|
| 458 |
+
try:
|
| 459 |
+
vals = [float(x) for x in grad_acc_seed_raw.split(",") if x.strip()]
|
| 460 |
+
for s, v in enumerate(vals, start=1):
|
| 461 |
+
if s < seed_start_stage:
|
| 462 |
+
grad_acc[s] = v
|
| 463 |
+
if grad_acc:
|
| 464 |
+
grad_acc_seeded = True
|
| 465 |
+
print(f"[repair] seeded graduation refs from env: "
|
| 466 |
+
f"{dict((k, round(v, 3)) for k, v in grad_acc.items())}",
|
| 467 |
+
flush=True)
|
| 468 |
+
except ValueError:
|
| 469 |
+
print(f"[repair] WARNING: bad SUDOKU_GRAD_ACC_SEED="
|
| 470 |
+
f"{grad_acc_seed_raw!r}; falling back to first-eval seeding",
|
| 471 |
+
flush=True)
|
| 472 |
+
|
| 473 |
+
def compute_deficits(frontier_stage, per_depth):
|
| 474 |
+
"""{depth t: graduation_acc[t] - current_acc} over earlier graduated
|
| 475 |
+
depths whose current candidate-set accuracy is measured.
|
| 476 |
+
|
| 477 |
+
Keyed on reasoning depth, not difficulty level: depth t's accuracy is
|
| 478 |
+
how well wave snapshot t is predicted, which is exactly what stage t
|
| 479 |
+
taught. A drop there means that propagation block has been forgotten."""
|
| 480 |
+
d = {}
|
| 481 |
+
for t in range(1, frontier_stage):
|
| 482 |
+
if t in grad_acc and per_depth.get(t, -1.0) >= 0:
|
| 483 |
+
d[t] = grad_acc[t] - per_depth.get(t, -1.0)
|
| 484 |
+
return d
|
| 485 |
+
|
| 486 |
+
def effective_margin(per_depth, frontier_stage):
|
| 487 |
+
"""Widen the repair trigger while the frontier itself is still weak."""
|
| 488 |
+
m = margin
|
| 489 |
+
if frontier_target_acc > 0:
|
| 490 |
+
f_acc = per_depth.get(frontier_stage, -1.0)
|
| 491 |
+
if 0.0 <= f_acc < frontier_target_acc:
|
| 492 |
+
# Only repair clearer regressions until the frontier is good.
|
| 493 |
+
m = max(m, margin + (frontier_target_acc - f_acc))
|
| 494 |
+
return m
|
| 495 |
+
|
| 496 |
+
def most_deficient(frontier_stage, per_depth, use_margin=None):
|
| 497 |
+
"""Most-regressed depth whose drop exceeds the margin, else None."""
|
| 498 |
+
d = compute_deficits(frontier_stage, per_depth)
|
| 499 |
+
if not d:
|
| 500 |
+
return None
|
| 501 |
+
m = margin if use_margin is None else use_margin
|
| 502 |
+
t = max(d, key=d.get)
|
| 503 |
+
return t if d[t] > m else None
|
| 504 |
+
|
| 505 |
+
with metric_writers.ensure_flushes(writer):
|
| 506 |
+
for step in range(start_step, config.max_steps):
|
| 507 |
+
if step % 10000 == 0:
|
| 508 |
+
print("Step:", step, flush=True)
|
| 509 |
+
|
| 510 |
+
i = curriculum.stage
|
| 511 |
+
# Target depth: the frontier when training normally, else the depth
|
| 512 |
+
# we are repairing. Difficulty is never gated, so a repair differs
|
| 513 |
+
# from a frontier step only in the recurrence depth it trains at:
|
| 514 |
+
# replaying depth t re-supervises wave snapshots 1..t on the same
|
| 515 |
+
# full-corpus batch distribution.
|
| 516 |
+
t = repair_stage if mode == "repair" else i
|
| 517 |
+
batch = sampler.sample_all(config.minibatch_size)
|
| 518 |
+
state, metrics = _run_train_step(
|
| 519 |
+
p_frontier[t], state, batch, dropout_rngs)
|
| 520 |
+
step_counts[t] += 1
|
| 521 |
+
if mode == "repair":
|
| 522 |
+
repair_steps_total += 1
|
| 523 |
+
|
| 524 |
+
for h in hooks:
|
| 525 |
+
h(step)
|
| 526 |
+
|
| 527 |
+
if math.isnan(metrics["loss"][0]):
|
| 528 |
+
print("Loss became nan; stopping.", flush=True)
|
| 529 |
+
break
|
| 530 |
+
|
| 531 |
+
if step % config.eval_every_steps == 0:
|
| 532 |
+
eval_metrics = evaluater.get_eval_metrics(
|
| 533 |
+
state, eval_data_iter, p_eval_step, config)
|
| 534 |
+
per_level = eval_metrics.pop("per_level_acc")
|
| 535 |
+
# Depth accuracy drives the controller. Changed-cells-only, so
|
| 536 |
+
# a slot that merely copies its predecessor scores zero credit.
|
| 537 |
+
per_depth = eval_metrics.pop("per_slot_acc_changed", {})
|
| 538 |
+
per_depth_all = eval_metrics.pop("per_slot_acc", {})
|
| 539 |
+
if not any(v >= 0 for v in per_depth.values()):
|
| 540 |
+
per_depth = per_depth_all
|
| 541 |
+
|
| 542 |
+
def _m(key):
|
| 543 |
+
v = eval_metrics.get(key, [])
|
| 544 |
+
return round(float(np.mean(v)), 4) if len(v) else -1.0
|
| 545 |
+
|
| 546 |
+
# Seed graduation refs for stages inherited from a checkpoint.
|
| 547 |
+
if not grad_acc_seeded:
|
| 548 |
+
for s in range(1, seed_start_stage):
|
| 549 |
+
acc_s = per_depth.get(s, -1.0)
|
| 550 |
+
if acc_s >= 0:
|
| 551 |
+
grad_acc[s] = float(acc_s)
|
| 552 |
+
grad_acc_seeded = True
|
| 553 |
+
print(f"[repair] step {step}: seeded graduation refs from "
|
| 554 |
+
f"resume: "
|
| 555 |
+
f"{dict((k, round(v, 3)) for k, v in grad_acc.items())}",
|
| 556 |
+
flush=True)
|
| 557 |
+
|
| 558 |
+
# Soft graduation refs: forgive chronic mild regression.
|
| 559 |
+
if grad_decay > 0 and grad_acc:
|
| 560 |
+
for s in list(grad_acc.keys()):
|
| 561 |
+
cur_s = per_depth.get(s, -1.0)
|
| 562 |
+
if cur_s >= 0 and cur_s < grad_acc[s]:
|
| 563 |
+
old = grad_acc[s]
|
| 564 |
+
grad_acc[s] = (
|
| 565 |
+
(1.0 - grad_decay) * grad_acc[s]
|
| 566 |
+
+ grad_decay * float(cur_s))
|
| 567 |
+
if step % (config.eval_every_steps * 5) == 0:
|
| 568 |
+
print(f"[repair] soft-grad depth {s}: "
|
| 569 |
+
f"{old:.3f}->{grad_acc[s]:.3f} "
|
| 570 |
+
f"(cur={cur_s:.3f})", flush=True)
|
| 571 |
+
|
| 572 |
+
frontier_acc = per_depth.get(curriculum.stage, -1.0)
|
| 573 |
+
eff_margin = effective_margin(per_depth, i)
|
| 574 |
+
deficits = compute_deficits(i, per_depth)
|
| 575 |
+
elapsed = max(1, step - start_step + 1)
|
| 576 |
+
repair_frac = repair_steps_total / float(elapsed)
|
| 577 |
+
bt_frozen = (
|
| 578 |
+
freeze_after_step > 0 and step >= freeze_after_step)
|
| 579 |
+
print(step, "stage", curriculum.stage,
|
| 580 |
+
"mode", mode,
|
| 581 |
+
"repair_stage", repair_stage,
|
| 582 |
+
"target_t", t,
|
| 583 |
+
"loss", round(float(metrics["loss"].mean()), 4),
|
| 584 |
+
"ce", round(float(metrics["ce_loss"].mean()), 4),
|
| 585 |
+
"aux_bce", round(float(metrics["aux_loss"].mean()), 4),
|
| 586 |
+
"| val_acc", _m("acc"), "loc_acc", _m("loc_acc"),
|
| 587 |
+
"val|loc", _m("val_given_loc_acc"),
|
| 588 |
+
"| frontier_acc", round(float(frontier_acc), 4),
|
| 589 |
+
"eff_margin", round(float(eff_margin), 4),
|
| 590 |
+
"| cand_bit_acc", _m("cand_bit_acc"),
|
| 591 |
+
"cand_set_acc", _m("cand_set_acc"),
|
| 592 |
+
"cand_set_chg", _m("cand_set_acc_changed"),
|
| 593 |
+
"| grad_acc", {k: round(v, 3) for k, v in grad_acc.items()},
|
| 594 |
+
"deficits", {k: round(v, 3) for k, v in deficits.items()},
|
| 595 |
+
"step_counts", dict(step_counts),
|
| 596 |
+
"repair_frac", round(repair_frac, 3),
|
| 597 |
+
"bt_frozen", bt_frozen,
|
| 598 |
+
flush=True)
|
| 599 |
+
|
| 600 |
+
with tf_summary_writer.as_default():
|
| 601 |
+
tf.summary.scalar("loss", metrics["loss"].mean(), step=step)
|
| 602 |
+
tf.summary.scalar("ce_loss", metrics["ce_loss"].mean(), step=step)
|
| 603 |
+
tf.summary.scalar("aux_bce_loss", metrics["aux_loss"].mean(), step=step)
|
| 604 |
+
tf.summary.scalar("curriculum_stage", curriculum.stage, step=step)
|
| 605 |
+
tf.summary.scalar("repair_mode", 1 if mode == "repair" else 0, step=step)
|
| 606 |
+
tf.summary.scalar("repair_stage", repair_stage or 0, step=step)
|
| 607 |
+
if frontier_acc >= 0:
|
| 608 |
+
tf.summary.scalar("frontier_depth_acc", frontier_acc, step=step)
|
| 609 |
+
tf.summary.scalar("eff_repair_margin", eff_margin, step=step)
|
| 610 |
+
for key in eval_metrics.keys():
|
| 611 |
+
tf.summary.scalar("eval_" + key,
|
| 612 |
+
np.array(eval_metrics[key]).mean(), step=step)
|
| 613 |
+
for lvl, v in per_level.items():
|
| 614 |
+
if v >= 0:
|
| 615 |
+
tf.summary.scalar(f"eval_acc_level_{lvl}", v, step=step)
|
| 616 |
+
for s, v in per_depth_all.items():
|
| 617 |
+
if v >= 0:
|
| 618 |
+
tf.summary.scalar(f"eval_cand_depth_{s}", v, step=step)
|
| 619 |
+
|
| 620 |
+
def _save_stage_ckpt(tag):
|
| 621 |
+
if config.save_checkpoint:
|
| 622 |
+
unrep = jax_utils.unreplicate(state)
|
| 623 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 624 |
+
workdir, unrep, step, keep=ckpt_keep, overwrite=True)
|
| 625 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 626 |
+
stage_ckpt_dir, unrep, step, keep=100,
|
| 627 |
+
overwrite=True, prefix=f"{tag}_")
|
| 628 |
+
|
| 629 |
+
# ---------------- Deficit-driven controller ----------------
|
| 630 |
+
# Freeze / duty-cycle: force frontier-only when budget exhausted.
|
| 631 |
+
duty_ok = (
|
| 632 |
+
max_repair_fraction <= 0
|
| 633 |
+
or repair_frac < max_repair_fraction)
|
| 634 |
+
if bt_frozen and mode == "repair":
|
| 635 |
+
print(f"[repair] step {step}: freeze_after="
|
| 636 |
+
f"{freeze_after_step}; leaving repair", flush=True)
|
| 637 |
+
mode = "frontier"
|
| 638 |
+
repair_stage = None
|
| 639 |
+
last_repair_return_step = step
|
| 640 |
+
if (not duty_ok) and mode == "repair":
|
| 641 |
+
print(f"[repair] step {step}: duty-cycle cap "
|
| 642 |
+
f"(repair_frac={repair_frac:.3f}>="
|
| 643 |
+
f"{max_repair_fraction}); return to frontier",
|
| 644 |
+
flush=True)
|
| 645 |
+
mode = "frontier"
|
| 646 |
+
repair_stage = None
|
| 647 |
+
last_repair_return_step = step
|
| 648 |
+
|
| 649 |
+
if mode == "frontier":
|
| 650 |
+
cooldown_ok = (
|
| 651 |
+
min_frontier_steps <= 0
|
| 652 |
+
or (step - last_repair_return_step) >= min_frontier_steps)
|
| 653 |
+
can_repair = (not bt_frozen) and duty_ok and cooldown_ok
|
| 654 |
+
worst = (most_deficient(i, per_depth, use_margin=eff_margin)
|
| 655 |
+
if can_repair else None)
|
| 656 |
+
if worst is not None:
|
| 657 |
+
mode = "repair"
|
| 658 |
+
repair_stage = worst
|
| 659 |
+
repair_started_at = step
|
| 660 |
+
repair_episode_start = step
|
| 661 |
+
print(f"[repair] step {step}: enter repair of depth "
|
| 662 |
+
f"{worst} (snapshot {worst} acc="
|
| 663 |
+
f"{per_depth.get(worst, -1.0):.3f} < grad "
|
| 664 |
+
f"{grad_acc.get(worst, -1.0):.3f} - "
|
| 665 |
+
f"eff_margin {eff_margin:.3f}; "
|
| 666 |
+
f"frontier_acc={frontier_acc:.3f})",
|
| 667 |
+
flush=True)
|
| 668 |
+
elif bt_frozen and most_deficient(
|
| 669 |
+
i, per_depth, use_margin=eff_margin) is not None:
|
| 670 |
+
if step % (config.eval_every_steps * 5) == 0:
|
| 671 |
+
print(f"[repair] step {step}: deficit present but "
|
| 672 |
+
f"BT frozen after {freeze_after_step}",
|
| 673 |
+
flush=True)
|
| 674 |
+
elif (not duty_ok) and most_deficient(
|
| 675 |
+
i, per_depth, use_margin=eff_margin) is not None:
|
| 676 |
+
if step % (config.eval_every_steps * 5) == 0:
|
| 677 |
+
print(f"[repair] step {step}: deficit present but "
|
| 678 |
+
f"duty-cycle cap "
|
| 679 |
+
f"(frac={repair_frac:.3f})",
|
| 680 |
+
flush=True)
|
| 681 |
+
elif (not cooldown_ok) and most_deficient(
|
| 682 |
+
i, per_depth, use_margin=eff_margin) is not None:
|
| 683 |
+
print(f"[repair] step {step}: deficit present but "
|
| 684 |
+
f"frontier cooldown "
|
| 685 |
+
f"({step - last_repair_return_step}/"
|
| 686 |
+
f"{min_frontier_steps}); staying on stage {i}",
|
| 687 |
+
flush=True)
|
| 688 |
+
elif curriculum.stage < curriculum.max_stage:
|
| 689 |
+
# Normal promotion (same rule as the standard loop).
|
| 690 |
+
steps_in_stage = step - stage_started_at
|
| 691 |
+
# A negative accuracy means "not measured this eval": it
|
| 692 |
+
# must not reset the plateau tracker nor satisfy the
|
| 693 |
+
# threshold/plateau rule. Only patience fires unmeasured.
|
| 694 |
+
measured = frontier_acc >= 0
|
| 695 |
+
if measured and frontier_acc > stage_best_acc + plateau_delta:
|
| 696 |
+
stage_best_acc = frontier_acc
|
| 697 |
+
stage_best_step = step
|
| 698 |
+
hit_threshold = measured and frontier_acc >= promote_threshold
|
| 699 |
+
stalled = (measured and plateau_steps > 0
|
| 700 |
+
and (step - stage_best_step) >= plateau_steps)
|
| 701 |
+
patience_over = steps_in_stage >= promote_patience
|
| 702 |
+
if steps_in_stage >= min_stage_steps and (
|
| 703 |
+
hit_threshold or stalled or patience_over):
|
| 704 |
+
reason = ("threshold" if hit_threshold
|
| 705 |
+
else "plateau" if stalled else "patience")
|
| 706 |
+
# Record this stage's graduation accuracy BEFORE moving on.
|
| 707 |
+
grad_acc[curriculum.stage] = float(frontier_acc)
|
| 708 |
+
curriculum.stage += 1
|
| 709 |
+
stage_started_at = step
|
| 710 |
+
stage_best_acc = -1.0
|
| 711 |
+
stage_best_step = step
|
| 712 |
+
print(f"[curriculum] step {step}: promote to stage "
|
| 713 |
+
f"{curriculum.stage} ({reason}; graduated "
|
| 714 |
+
f"depth {curriculum.stage - 1} cand-set "
|
| 715 |
+
f"acc={frontier_acc:.3f})", flush=True)
|
| 716 |
+
_save_stage_ckpt(f"stage{curriculum.stage}")
|
| 717 |
+
elif (frontier_target_acc > 0
|
| 718 |
+
and 0.0 <= frontier_acc < frontier_target_acc
|
| 719 |
+
and step % (config.eval_every_steps * 5) == 0):
|
| 720 |
+
print(f"[frontier] step {step}: depth {i} "
|
| 721 |
+
f"acc={frontier_acc:.3f} "
|
| 722 |
+
f"< target {frontier_target_acc:.3f}; "
|
| 723 |
+
f"keeping frontier priority",
|
| 724 |
+
flush=True)
|
| 725 |
+
else: # mode == "repair"
|
| 726 |
+
r = repair_stage
|
| 727 |
+
cur = per_depth.get(r, -1.0)
|
| 728 |
+
ref = grad_acc.get(r, -1.0)
|
| 729 |
+
recovered = cur >= (ref - margin)
|
| 730 |
+
capped = (step - repair_started_at) >= max_repair_steps
|
| 731 |
+
if recovered or capped:
|
| 732 |
+
why = "recovered" if recovered else "cap"
|
| 733 |
+
print(f"[repair] step {step}: depth {r} done ({why}; "
|
| 734 |
+
f"snapshot acc={cur:.3f} vs grad {ref:.3f})",
|
| 735 |
+
flush=True)
|
| 736 |
+
_save_stage_ckpt(f"repair{r}")
|
| 737 |
+
# Re-scan: chain to the next most-deficient stage, or
|
| 738 |
+
# return to the frontier and credit repair time back to
|
| 739 |
+
# the frontier stage's patience clock.
|
| 740 |
+
nxt = None
|
| 741 |
+
if (not bt_frozen) and duty_ok:
|
| 742 |
+
nxt = most_deficient(
|
| 743 |
+
i, per_depth, use_margin=eff_margin)
|
| 744 |
+
if nxt is not None:
|
| 745 |
+
repair_stage = nxt
|
| 746 |
+
repair_started_at = step
|
| 747 |
+
print(f"[repair] step {step}: chain to stage {nxt}",
|
| 748 |
+
flush=True)
|
| 749 |
+
else:
|
| 750 |
+
mode = "frontier"
|
| 751 |
+
repair_stage = None
|
| 752 |
+
last_repair_return_step = step
|
| 753 |
+
stage_started_at += (step - repair_episode_start)
|
| 754 |
+
|
| 755 |
+
if config.save_checkpoint and step > 0 and step % config.save_every_steps == 0:
|
| 756 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 757 |
+
workdir, jax_utils.unreplicate(state), step,
|
| 758 |
+
keep=ckpt_keep, overwrite=True)
|
| 759 |
+
|
| 760 |
+
if config.save_checkpoint:
|
| 761 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 762 |
+
workdir, jax_utils.unreplicate(state), config.max_steps,
|
| 763 |
+
keep=ckpt_keep, overwrite=True)
|
code/wavecurriculum_run/train/trainer.py
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2024 The Google Research Authors.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""Transformer LM trainer."""
|
| 17 |
+
|
| 18 |
+
import functools
|
| 19 |
+
|
| 20 |
+
from clu import periodic_actions
|
| 21 |
+
from flax.training import common_utils
|
| 22 |
+
from flax.training import train_state
|
| 23 |
+
import jax
|
| 24 |
+
from jax import numpy as jnp
|
| 25 |
+
import numpy as np
|
| 26 |
+
import optax
|
| 27 |
+
import ml_collections
|
| 28 |
+
|
| 29 |
+
from train import model
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def get_state(config, net, initial_variables):
|
| 33 |
+
"""Get the train state given an experiment config, a model and initial variables.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
config: A ConfigDict containing the configuration for the experiment.
|
| 37 |
+
net: The model to use for training.
|
| 38 |
+
initial_variables: The initial variables for the model.
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
A tuple containing the train state and the learning rate schedule.
|
| 42 |
+
"""
|
| 43 |
+
# Learning rate schedule
|
| 44 |
+
lr_scheduler_fn = functools.partial(
|
| 45 |
+
lr_scheduler,
|
| 46 |
+
learning_rate=config.learning_rate,
|
| 47 |
+
warmup_tokens=config.warmup_tokens,
|
| 48 |
+
final_tokens=config.max_steps,
|
| 49 |
+
config=config,
|
| 50 |
+
)
|
| 51 |
+
# Optimizer
|
| 52 |
+
optim_fn = optax.adamw(
|
| 53 |
+
lr_scheduler_fn, weight_decay=config.weight_decay, b1=0.9, b2=0.95
|
| 54 |
+
)
|
| 55 |
+
# Clip the gradients to prevent exploding gradients.
|
| 56 |
+
optimizer = optax.chain(optax.clip_by_global_norm(1), optim_fn)
|
| 57 |
+
|
| 58 |
+
# Initialize the train state
|
| 59 |
+
state = train_state.TrainState.create(
|
| 60 |
+
apply_fn=net.apply, params=initial_variables["params"],
|
| 61 |
+
tx=optimizer
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
return state, lr_scheduler_fn
|
| 65 |
+
|
| 66 |
+
def lr_scheduler(
|
| 67 |
+
n_tokens: int, learning_rate: float, warmup_tokens: int, final_tokens: int,
|
| 68 |
+
config: ml_collections.ConfigDict,
|
| 69 |
+
) -> float:
|
| 70 |
+
"""Learning rate scheduler, adapted from Mikhail Grankin.
|
| 71 |
+
|
| 72 |
+
The learning rate schedule is cosine decay with a warmup period.
|
| 73 |
+
|
| 74 |
+
The learning rate starts at 0 and linearly increases to the given learning
|
| 75 |
+
rate over the warmup period. After the warmup period, the learning rate
|
| 76 |
+
decays according to a cosine schedule, with the given learning rate as the
|
| 77 |
+
maximum value.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
n_tokens: The number of tokens processed so far.
|
| 81 |
+
learning_rate: The initial learning rate.
|
| 82 |
+
warmup_tokens: The number of tokens to warm up over.
|
| 83 |
+
final_tokens: The total number of tokens to process.
|
| 84 |
+
config: A ConfigDict containing the configuration for the learning rate
|
| 85 |
+
schedule.
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
The learning rate at the given point in the schedule.
|
| 89 |
+
"""
|
| 90 |
+
# Decay the learning rate based on our progress.
|
| 91 |
+
progress = (n_tokens - warmup_tokens) / max(
|
| 92 |
+
1, final_tokens - warmup_tokens,
|
| 93 |
+
)
|
| 94 |
+
lr_mult = jnp.where(
|
| 95 |
+
n_tokens < warmup_tokens,
|
| 96 |
+
# Linear warmup.
|
| 97 |
+
n_tokens / jnp.fmax(1, warmup_tokens),
|
| 98 |
+
# Cosine learning rate decay.
|
| 99 |
+
jnp.fmax(config.end_lr_factor, 0.5 * (1.0 + jnp.cos(np.pi * progress))),
|
| 100 |
+
)
|
| 101 |
+
return learning_rate * lr_mult
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def get_metrics_report_progress(config, workdir, writer):
|
| 105 |
+
"""
|
| 106 |
+
Get the metrics for reporting progress during training.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
config: The configuration for the experiment.
|
| 110 |
+
workdir: The directory for storing the logs.
|
| 111 |
+
writer: The writer object for recording the metrics.
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
hooks: List of hooks for tracking progress.
|
| 115 |
+
report_progress: Object for reporting progress.
|
| 116 |
+
train_metrics: List of training metrics.
|
| 117 |
+
"""
|
| 118 |
+
hooks = []
|
| 119 |
+
|
| 120 |
+
# Initialize the report progress object
|
| 121 |
+
report_progress = periodic_actions.ReportProgress(
|
| 122 |
+
num_train_steps=config.max_steps, writer=writer)
|
| 123 |
+
|
| 124 |
+
# Add metrics for profiling if the process index is 0
|
| 125 |
+
if jax.process_index() == 0:
|
| 126 |
+
hooks += [report_progress,
|
| 127 |
+
periodic_actions.Profile(logdir=workdir, num_profile_steps=5)]
|
| 128 |
+
|
| 129 |
+
# Initialize the list of training metrics
|
| 130 |
+
train_metrics = []
|
| 131 |
+
|
| 132 |
+
return hooks, report_progress, train_metrics
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def get_input_start_index(batch, config):
|
| 136 |
+
inputs = jax.tree_util.tree_map(np.asarray, batch[0])
|
| 137 |
+
puzzles = jax.tree_util.tree_map(np.asarray, batch[1])
|
| 138 |
+
start_index = jax.tree_util.tree_map(np.asarray, batch[2])
|
| 139 |
+
levels = jax.tree_util.tree_map(np.asarray, batch[3])
|
| 140 |
+
cand_targets = jax.tree_util.tree_map(np.asarray, batch[4])
|
| 141 |
+
return inputs, puzzles, start_index, levels, cand_targets
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def train_one_step(p_train_step, config, state, step, dropout_rngs, train_data_iter):
|
| 145 |
+
"""
|
| 146 |
+
Single step of the training loop.
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
p_train_step: The training step function.
|
| 150 |
+
config: The experiment configuration.
|
| 151 |
+
state: The train state.
|
| 152 |
+
step: The step number.
|
| 153 |
+
dropout_rngs: The dropout random number generator.
|
| 154 |
+
train_data_iter: The iterator for the train data.
|
| 155 |
+
|
| 156 |
+
Returns:
|
| 157 |
+
The updated train state and train metrics.
|
| 158 |
+
"""
|
| 159 |
+
with jax.profiler.StepTraceAnnotation("train", step_num=step):
|
| 160 |
+
# Get the next batch from the iterator
|
| 161 |
+
batch = next(train_data_iter)
|
| 162 |
+
# Extract the inputs, start index and difficulty level from the batch
|
| 163 |
+
inputs, _, start_index, levels, cand_targets = get_input_start_index(batch, config)
|
| 164 |
+
# Shard across the devices
|
| 165 |
+
inputs = common_utils.shard(jax.tree_util.tree_map(np.asarray, inputs))
|
| 166 |
+
start_index = common_utils.shard(jax.tree_util.tree_map(np.asarray, start_index))
|
| 167 |
+
levels = common_utils.shard(jax.tree_util.tree_map(np.asarray, levels))
|
| 168 |
+
# Explicit reshape (not common_utils.shard) so a zero-width slot dim
|
| 169 |
+
# (K=0 no-latent baseline: cand_targets is (bs, 0, 81)) shards without
|
| 170 |
+
# the ambiguous -1 inference. Identical to shard() when K>0.
|
| 171 |
+
cand_targets = np.asarray(cand_targets)
|
| 172 |
+
_nd = jax.local_device_count()
|
| 173 |
+
cand_targets = cand_targets.reshape(
|
| 174 |
+
(_nd, cand_targets.shape[0] // _nd) + cand_targets.shape[1:])
|
| 175 |
+
|
| 176 |
+
# Run the training step
|
| 177 |
+
state, metrics, _ = p_train_step(
|
| 178 |
+
state, inputs, start_index, levels, cand_targets, dropout_rng=dropout_rngs
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
return state, metrics
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def build_latent_state(inputs, start_index, levels, config, num_passes,
|
| 185 |
+
apply_fn, rngs=None):
|
| 186 |
+
"""Build the continuous latent thoughts (Coconut/ATC-style recurrence).
|
| 187 |
+
|
| 188 |
+
Pass 0 seeds z_1 from the last-layer hidden at the last clue token; pass j
|
| 189 |
+
reads the hidden at latent slot j to produce z_{j+1}. Slot j is active for
|
| 190 |
+
an example only when j < k, with k = clip(level - 2, 1, K) (difficulty-
|
| 191 |
+
matched latent budget). Gradients flow through all passes (full BPTT).
|
| 192 |
+
|
| 193 |
+
Returns (latent_vals, slot_pos, active_full).
|
| 194 |
+
"""
|
| 195 |
+
num_slots = int(config.num_latent_slots)
|
| 196 |
+
bs = inputs.shape[0]
|
| 197 |
+
bidx = jnp.arange(bs)
|
| 198 |
+
|
| 199 |
+
si3 = 3 * start_index.reshape(-1) # (bs,)
|
| 200 |
+
slot_pos = si3[:, None] + jnp.arange(num_slots)[None, :] # (bs, K)
|
| 201 |
+
if getattr(config, "cand_slot_mode", "level") == "depth":
|
| 202 |
+
# Every slot the recurrence fills is active, uniformly over the batch.
|
| 203 |
+
k = jnp.full((bs,), max(min(num_passes, num_slots), 1), dtype=jnp.int32)
|
| 204 |
+
else:
|
| 205 |
+
k = jnp.clip(levels.reshape(-1) - 2, 1, num_slots) # (bs,)
|
| 206 |
+
active_full = jnp.arange(num_slots)[None, :] < k[:, None] # (bs, K)
|
| 207 |
+
|
| 208 |
+
latent_vals = jnp.zeros((bs, num_slots, config.emb_dim), dtype=config.dtype)
|
| 209 |
+
|
| 210 |
+
for j in range(num_passes):
|
| 211 |
+
act_j = active_full & (jnp.arange(num_slots)[None, :] < j)
|
| 212 |
+
_, hidden, _ = apply_fn(inputs, latent_vals, slot_pos, act_j, rngs)
|
| 213 |
+
src = si3 - 1 + j # j=0: last clue token; j>0: latent slot j-1
|
| 214 |
+
z = hidden[bidx, src]
|
| 215 |
+
latent_vals = latent_vals.at[:, j].set(z.astype(config.dtype))
|
| 216 |
+
|
| 217 |
+
return latent_vals, slot_pos, active_full
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def train_step(state, batch, start_index, levels, cand_targets, config,
|
| 221 |
+
hyperparams, learning_rate_fn, num_passes, dropout_rng=None,
|
| 222 |
+
backtrack=False):
|
| 223 |
+
"""One step of the training loop.
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
state: Train state.
|
| 227 |
+
batch: Input batch (bs, 3*81 + K) with latent placeholder slots.
|
| 228 |
+
start_index: Number of clue cells per example.
|
| 229 |
+
levels: Puzzle difficulty level (3..8) per example.
|
| 230 |
+
config: Model config.
|
| 231 |
+
hyperparams: Hyperparameter dictionary.
|
| 232 |
+
learning_rate_fn: Learning rate function.
|
| 233 |
+
num_passes: Number of latent recurrence passes (= curriculum stage).
|
| 234 |
+
dropout_rng: RNG used for dropout.
|
| 235 |
+
|
| 236 |
+
Returns:
|
| 237 |
+
A new train state, train metrics, and computed model predictions.
|
| 238 |
+
"""
|
| 239 |
+
num_slots = int(config.num_latent_slots)
|
| 240 |
+
# Extract inputs and labels from the batch
|
| 241 |
+
inputs = batch[:, :-1]
|
| 242 |
+
label = batch[:, 1:]
|
| 243 |
+
|
| 244 |
+
# Update dropout_rng
|
| 245 |
+
dropout_rng = jax.random.fold_in(dropout_rng, state.step)
|
| 246 |
+
dropout_rng_dict = {"dropout": dropout_rng}
|
| 247 |
+
|
| 248 |
+
def loss_fn(params):
|
| 249 |
+
"""Compute the loss function."""
|
| 250 |
+
net = model.TransformerLMHeadModel(config)
|
| 251 |
+
|
| 252 |
+
def apply_fn(x, lv, lp, la, rngs):
|
| 253 |
+
return net.apply({"params": params}, x, latent_values=lv,
|
| 254 |
+
latent_positions=lp, latent_active=la,
|
| 255 |
+
rngs=rngs)
|
| 256 |
+
|
| 257 |
+
latent_vals, slot_pos, active_full = build_latent_state(
|
| 258 |
+
inputs, start_index, levels, config, num_passes, apply_fn,
|
| 259 |
+
rngs=dropout_rng_dict)
|
| 260 |
+
|
| 261 |
+
pred_logits, _, cand_logits = apply_fn(
|
| 262 |
+
inputs, latent_vals, slot_pos, active_full, dropout_rng_dict)
|
| 263 |
+
|
| 264 |
+
label_one_hot = jax.nn.one_hot(label, num_classes=config.vocab_size)
|
| 265 |
+
|
| 266 |
+
# The variables label_one_hot and pred_logits both are 3-dimensional tensors with
|
| 267 |
+
# first axis corresponding to batch size, second correspondingn to sequence length
|
| 268 |
+
# and third corresponding to the row/column/value at a particular cell
|
| 269 |
+
assert label_one_hot.shape == pred_logits.shape, ("one hot label shape",
|
| 270 |
+
label_one_hot.shape,
|
| 271 |
+
label.shape,
|
| 272 |
+
pred_logits.shape)
|
| 273 |
+
|
| 274 |
+
# Calculate the cross-entropy loss along the last axis
|
| 275 |
+
pred_logits_sol = pred_logits[:, :, :]
|
| 276 |
+
label_one_hot_sol = label_one_hot[:, :, :]
|
| 277 |
+
|
| 278 |
+
ce_loss = optax.softmax_cross_entropy(
|
| 279 |
+
logits=pred_logits_sol[:, :, :], labels=label_one_hot_sol[:, :, :]
|
| 280 |
+
)
|
| 281 |
+
# assert ce_loss.ndim == 2, ("ce_loss", ce_loss.shape)
|
| 282 |
+
|
| 283 |
+
# Apply masking to the loss: supervise only the solution region,
|
| 284 |
+
# which now starts K latent slots after the clue block.
|
| 285 |
+
mask = np.repeat(
|
| 286 |
+
np.arange(len(ce_loss[0])).reshape(1, -1), len(ce_loss), axis=0
|
| 287 |
+
)
|
| 288 |
+
mask = (mask >= 3 * start_index + num_slots)
|
| 289 |
+
|
| 290 |
+
# Per-token mean (not per-example sum): keeps the LM CE on the same
|
| 291 |
+
# O(1) scale as the per-digit BCE below, so aux_cand_weight~1 actually
|
| 292 |
+
# balances the two instead of the candidate signal being swamped.
|
| 293 |
+
ce_denom = jnp.maximum(mask.sum(), 1.0)
|
| 294 |
+
avg_ce_loss = (ce_loss * mask).sum() / ce_denom
|
| 295 |
+
|
| 296 |
+
# ---- Auxiliary multi-candidate BCE loss on the latent slots ----
|
| 297 |
+
# cand_targets: (bs, K, 81) int bitmask (bit d-1 set <=> digit d is a
|
| 298 |
+
# candidate at that slot's stage; slot j already aligned to the puzzle's
|
| 299 |
+
# k=level-2 budget in the data pipeline). Expand to (bs,K,81,9) multi-hot.
|
| 300 |
+
aux_weight = float(getattr(hyperparams, "aux_cand_weight", 1.0))
|
| 301 |
+
if aux_weight == 0.0:
|
| 302 |
+
# Instance arm: the superposition lives in the varying targets, not
|
| 303 |
+
# in a set head. Drop the whole BCE graph so nothing but the LM CE
|
| 304 |
+
# shapes the latents.
|
| 305 |
+
zero = jnp.zeros((), dtype=pred_logits.dtype)
|
| 306 |
+
return avg_ce_loss, (pred_logits, avg_ce_loss, zero)
|
| 307 |
+
|
| 308 |
+
bits = jnp.arange(9)
|
| 309 |
+
cand_multi_hot = ((cand_targets[..., None].astype(jnp.int32)
|
| 310 |
+
>> bits) & 1).astype(cand_logits.dtype) # (bs,K,81,9)
|
| 311 |
+
# Positive-weighted BCE. Candidate masks are sparse (~1-3 of 9 digits
|
| 312 |
+
# "on"), so plain BCE collapses to predicting all-zeros. Up-weighting
|
| 313 |
+
# the positive (candidate-present) term by pos_weight counteracts the
|
| 314 |
+
# imbalance and forces the head to actually predict the candidate set.
|
| 315 |
+
pos_weight = float(getattr(hyperparams, "aux_pos_weight", 5.0))
|
| 316 |
+
log_p = jax.nn.log_sigmoid(cand_logits) # log sigmoid(x)
|
| 317 |
+
log_1mp = jax.nn.log_sigmoid(-cand_logits) # log(1 - sigmoid(x))
|
| 318 |
+
bce = -(pos_weight * cand_multi_hot * log_p
|
| 319 |
+
+ (1.0 - cand_multi_hot) * log_1mp) # (bs,K,81,9)
|
| 320 |
+
# Only the empty cells are supervised: clue cells were sentinel-zeroed
|
| 321 |
+
# in the data pipeline, so any cell whose target row is all-zero is a
|
| 322 |
+
# clue and must not contribute. This makes the effective target
|
| 323 |
+
# (#empty cells) x 9 per puzzle rather than 81 x 9.
|
| 324 |
+
cell_mask = (cand_targets > 0).astype(cand_logits.dtype) # (bs, K, 81)
|
| 325 |
+
# Delta weighting: consecutive stages are near-duplicates (at K=12 only
|
| 326 |
+
# ~14% of cells change per stage, so ~97% of the target bits are copies
|
| 327 |
+
# of the previous slot). Weighting the unchanged cells below 1 stops the
|
| 328 |
+
# objective from being satisfied by echoing slot j-1. Slot 0 has no
|
| 329 |
+
# predecessor, so it is fully weighted.
|
| 330 |
+
delta_bg = float(getattr(hyperparams, "aux_delta_bg", 1.0))
|
| 331 |
+
if delta_bg != 1.0:
|
| 332 |
+
changed = jnp.concatenate(
|
| 333 |
+
[jnp.ones_like(cand_targets[:, :1], dtype=bool),
|
| 334 |
+
cand_targets[:, 1:] != cand_targets[:, :-1]], axis=1)
|
| 335 |
+
cell_w = cell_mask * (delta_bg + (1.0 - delta_bg)
|
| 336 |
+
* changed.astype(cand_logits.dtype))
|
| 337 |
+
else:
|
| 338 |
+
cell_w = cell_mask
|
| 339 |
+
cell_denom = jnp.maximum(cell_w.sum(axis=2), 1e-6) # (bs, K)
|
| 340 |
+
bce_per_cell = bce.mean(axis=3) # (bs, K, 81), mean over 9 digits
|
| 341 |
+
bce_per_slot = ((bce_per_cell * cell_w).sum(axis=2)
|
| 342 |
+
/ cell_denom) # (bs, K), mean over empty cells
|
| 343 |
+
if backtrack:
|
| 344 |
+
# Strict stage-replay: supervise ONLY the stage-t readout = the last
|
| 345 |
+
# active latent slot per example (index k-1), whose candidate target
|
| 346 |
+
# is that stage's grid. Earlier slots and the LM CE are dropped, so
|
| 347 |
+
# this step purely re-derives "apply f exactly t times -> stage-t
|
| 348 |
+
# readout." Injection above still spans all active slots, so the
|
| 349 |
+
# recurrence reaching slot k-1 is intact.
|
| 350 |
+
k_per = active_full.sum(axis=1) # (bs,)
|
| 351 |
+
last_idx = (k_per - 1)[:, None] # (bs, 1)
|
| 352 |
+
slot_sel = ((jnp.arange(num_slots)[None, :] == last_idx)
|
| 353 |
+
& active_full) # (bs, K)
|
| 354 |
+
slot_active = slot_sel.astype(bce_per_slot.dtype)
|
| 355 |
+
else:
|
| 356 |
+
slot_active = active_full.astype(bce_per_slot.dtype) # (bs, K)
|
| 357 |
+
aux_denom = jnp.maximum(slot_active.sum(), 1.0)
|
| 358 |
+
avg_aux_loss = (bce_per_slot * slot_active).sum() / aux_denom
|
| 359 |
+
|
| 360 |
+
# Backtrack (replay) steps train only from the readout (no LM CE).
|
| 361 |
+
ce_term = 0.0 if backtrack else avg_ce_loss
|
| 362 |
+
total_loss = ce_term + aux_weight * avg_aux_loss
|
| 363 |
+
|
| 364 |
+
return total_loss, (pred_logits, avg_ce_loss, avg_aux_loss)
|
| 365 |
+
|
| 366 |
+
# Compute the learning rate and perform gradient descent
|
| 367 |
+
step = state.step
|
| 368 |
+
lr = learning_rate_fn(step)
|
| 369 |
+
(loss, aux), grads = jax.value_and_grad(loss_fn,
|
| 370 |
+
has_aux=True)(state.params)
|
| 371 |
+
pred_logits, ce_loss, aux_loss = aux
|
| 372 |
+
grads = jax.lax.pmean(grads, "batch")
|
| 373 |
+
new_state = state.apply_gradients(grads=grads)
|
| 374 |
+
|
| 375 |
+
# Update training metrics
|
| 376 |
+
metrics = {
|
| 377 |
+
"step": step, "loss": loss, "learning_rate": lr,
|
| 378 |
+
"ce_loss": ce_loss,
|
| 379 |
+
"aux_loss": aux_loss,
|
| 380 |
+
"pred_logits": pred_logits, "weights": inputs.shape[0]
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
return new_state, metrics, pred_logits
|
code/wavecurriculum_run/verify_uniform_loader.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Verify the on-the-fly uniform instance sampler on the real stage-1 masks.
|
| 2 |
+
|
| 3 |
+
Replicates SudokuDataset.uniform_instance_values / apply_instance with numpy
|
| 4 |
+
only, then checks the three things the experiment depends on:
|
| 5 |
+
1. the clue block is untouched, so the input prompt is fixed per puzzle
|
| 6 |
+
2. every drawn digit lies in that cell's candidate set
|
| 7 |
+
3. over N draws each candidate of each cell turns up a near-equal number of
|
| 8 |
+
times, and at least 5 times
|
| 9 |
+
"""
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
PUZ = ("/scratch/users/gatmiry/llm-reasoning-logic-puzzles/sudoku-code/"
|
| 13 |
+
"datasets/train_sudoku_puzzles.npy")
|
| 14 |
+
CAND = "/tmp/sudoku_s12/train_cand_masks.npy"
|
| 15 |
+
STAGE = 0
|
| 16 |
+
N_DRAWS = 64
|
| 17 |
+
K, LATENT_ID = 12, 10
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def load_inputs(n):
|
| 21 |
+
raw = np.load(PUZ, mmap_mode="r")
|
| 22 |
+
rows = np.asarray(raw[:n]).astype(np.int64)
|
| 23 |
+
return np.delete(rows[:, 1:], np.arange(81) * 4 + 3, axis=1), rows[:, 0]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def uniform_instance_values(mask, rng):
|
| 27 |
+
"""Same argmax-of-random-keys draw as the loader."""
|
| 28 |
+
m = mask.astype(np.int64)
|
| 29 |
+
bits = ((m[:, None] >> np.arange(9)) & 1).astype(np.float64)
|
| 30 |
+
keys = rng.random_sample((81, 9)) * bits
|
| 31 |
+
vals = (keys.argmax(1) + 1).astype(np.int8)
|
| 32 |
+
return np.where(m > 0, vals, 0)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def apply_instance(seq, vals):
|
| 36 |
+
seq = seq.copy()
|
| 37 |
+
cells = seq[0::3] * 9 + seq[1::3]
|
| 38 |
+
new = vals[cells]
|
| 39 |
+
seq[2::3] = np.where(new > 0, new, seq[2::3])
|
| 40 |
+
return seq
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def digits_of(m):
|
| 44 |
+
return [d for d in range(1, 10) if int(m) & (1 << (d - 1))]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def main():
|
| 48 |
+
n_show = 500
|
| 49 |
+
inputs, si_all = load_inputs(n_show)
|
| 50 |
+
masks = np.load(CAND, mmap_mode="r")
|
| 51 |
+
rng = np.random.RandomState(0)
|
| 52 |
+
|
| 53 |
+
p = 3
|
| 54 |
+
si, seq = int(si_all[p]), inputs[p]
|
| 55 |
+
mask = np.asarray(masks[p, STAGE])
|
| 56 |
+
tr = seq.reshape(-1, 3)
|
| 57 |
+
empty = (tr[si:, 0] * 9 + tr[si:, 1])
|
| 58 |
+
|
| 59 |
+
drawn = [apply_instance(seq, uniform_instance_values(mask, rng))
|
| 60 |
+
for _ in range(N_DRAWS)]
|
| 61 |
+
|
| 62 |
+
print(f"puzzle p={p}: {si} clues, {len(empty)} empty cells, "
|
| 63 |
+
f"{N_DRAWS} uniform draws")
|
| 64 |
+
|
| 65 |
+
clue_ok = all(np.array_equal(d[:3 * si], seq[:3 * si]) for d in drawn)
|
| 66 |
+
print(f" clue block identical in all {N_DRAWS} draws: {clue_ok}")
|
| 67 |
+
|
| 68 |
+
bad = sum(int(d[2::3][j + si]) not in digits_of(mask[empty[j]])
|
| 69 |
+
for d in drawn for j in range(len(empty)))
|
| 70 |
+
print(f" drawn digits outside the candidate set: {bad}")
|
| 71 |
+
|
| 72 |
+
blk = np.stack([d[2::3] for d in drawn])[:, si:] # (N, n_empty)
|
| 73 |
+
|
| 74 |
+
print(f"\ninstance = one digit for every empty cell. First 10 cells:")
|
| 75 |
+
print(" cells: " + " ".join(f"({c // 9},{c % 9})" for c in empty[:10]))
|
| 76 |
+
for i in range(4):
|
| 77 |
+
print(f" i{i + 1}: " + " ".join(
|
| 78 |
+
f"{int(x)}" for x in blk[i, :10]))
|
| 79 |
+
print(" S: " + " ".join(f"{digits_of(mask[c])}" for c in empty[:4])
|
| 80 |
+
+ " ...")
|
| 81 |
+
|
| 82 |
+
print(f"\nreading DOWN a cell's column over {N_DRAWS} draws:")
|
| 83 |
+
print(f" {'cell':>8} {'|S|':>3} {'S':<16}{'counts':<34}min")
|
| 84 |
+
shown = 0
|
| 85 |
+
worst = 10 ** 9
|
| 86 |
+
for j, c in enumerate(empty):
|
| 87 |
+
S = digits_of(mask[c])
|
| 88 |
+
if len(S) < 2:
|
| 89 |
+
continue
|
| 90 |
+
v, k = np.unique(blk[:, j], return_counts=True)
|
| 91 |
+
cnt = {int(a): int(b) for a, b in zip(v, k)}
|
| 92 |
+
worst = min(worst, min(cnt.get(d, 0) for d in S))
|
| 93 |
+
shown += 1
|
| 94 |
+
if shown > 8:
|
| 95 |
+
continue
|
| 96 |
+
cs = " ".join(f"{d}:{cnt.get(d, 0)}" for d in S)
|
| 97 |
+
print(f" ({c // 9},{c % 9}) {len(S):>3} {str(S):<16}{cs:<34}"
|
| 98 |
+
f"{min(cnt.get(d, 0) for d in S)}")
|
| 99 |
+
print(f" ... {shown} multi-candidate cells; rarest candidate anywhere in "
|
| 100 |
+
f"this puzzle appeared {worst}x (need >=5)")
|
| 101 |
+
|
| 102 |
+
print(f"\nover {n_show} puzzles:")
|
| 103 |
+
ge5 = tot = 0
|
| 104 |
+
spreads = []
|
| 105 |
+
for q in range(n_show):
|
| 106 |
+
m = np.asarray(masks[q, STAGE])
|
| 107 |
+
b = np.stack([uniform_instance_values(m, rng) for _ in range(N_DRAWS)])
|
| 108 |
+
for c in range(81):
|
| 109 |
+
S = digits_of(m[c])
|
| 110 |
+
if len(S) < 2:
|
| 111 |
+
continue
|
| 112 |
+
v, k = np.unique(b[:, c], return_counts=True)
|
| 113 |
+
cnt = {int(a): int(b_) for a, b_ in zip(v, k)}
|
| 114 |
+
tot += len(S)
|
| 115 |
+
ge5 += sum(1 for d in S if cnt.get(d, 0) >= 5)
|
| 116 |
+
pr = np.array([cnt.get(d, 0) for d in S], dtype=float)
|
| 117 |
+
pr /= pr.sum()
|
| 118 |
+
nz = pr[pr > 0]
|
| 119 |
+
spreads.append(float(-(nz * np.log(nz)).sum()) / np.log(len(S)))
|
| 120 |
+
print(f" (cell, candidate) pairs seen >=5 times: {ge5 / tot:.4f}")
|
| 121 |
+
print(f" mean spread H(p)/log|S|: {np.mean(spreads):.4f} "
|
| 122 |
+
f"(1.0 = uniform superposition)")
|
| 123 |
+
|
| 124 |
+
full = np.concatenate([drawn[0][:3 * si],
|
| 125 |
+
np.full(K, LATENT_ID, dtype=drawn[0].dtype),
|
| 126 |
+
drawn[0][3 * si:]])
|
| 127 |
+
print(f"\ntoken sequence: {len(full)} = {3 * si} clue + {K} latent + "
|
| 128 |
+
f"{3 * (81 - si)} output")
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
if __name__ == "__main__":
|
| 132 |
+
main()
|