Add README and training/generation code
Browse files- README.md +72 -0
- code/build_instance_offsets.py +46 -0
- code/build_superposition_dataset.py +128 -0
- code/env_paths.sh +77 -0
- code/launch_instance_latent.sh +73 -0
- code/level_instance_stats.py +94 -0
- code/sbatch_instance_latent.sh +142 -0
- code/sbatch_instance_latent_bt.sh +145 -0
- code/show_instance_stages.py +94 -0
- code/show_superposition_example.py +102 -0
- code/smoke_instance_loader.py +74 -0
- code/superposition_instances.py +274 -0
- code/train/data.py +537 -0
- code/train/evaluater.py +455 -0
- code/train/main.py +320 -0
- code/train/model.py +232 -0
- code/train/train_and_evaluate.py +386 -0
- code/train/train_backtrack.py +773 -0
- code/train/trainer.py +383 -0
- code/verify_superposition_instances.py +98 -0
README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
tags:
|
| 4 |
+
- sudoku
|
| 5 |
+
- reasoning
|
| 6 |
+
- curriculum
|
| 7 |
+
- jax
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Sudoku superposition instances
|
| 11 |
+
|
| 12 |
+
Concrete board assignments for a 12-stage Sudoku latent curriculum.
|
| 13 |
+
Each stage keeps a candidate set per cell; this dataset materializes
|
| 14 |
+
those sets as ordinary `(row, col, value)` sequences so training is
|
| 15 |
+
standard next-token cross-entropy (no multi-hot candidate head).
|
| 16 |
+
|
| 17 |
+
Repo: [Avra98/Sudoku_superposition](https://huggingface.co/Avra98/Sudoku_superposition)
|
| 18 |
+
|
| 19 |
+
## Dataset
|
| 20 |
+
|
| 21 |
+
| split | puzzles | instances | mean / puzzle |
|
| 22 |
+
| --- | ---: | ---: | ---: |
|
| 23 |
+
| train | 1,804,462 | 89,236,838 | 49.45 |
|
| 24 |
+
| test | 99,999 | 4,945,532 | 49.46 |
|
| 25 |
+
|
| 26 |
+
Mean instances per stage (train, stage 0 → 11):
|
| 27 |
+
`6.28, 5.96, 5.65, 5.34, 5.02, 4.68, 4.32, 3.88, 3.33, 2.58, 1.41, 1.00`
|
| 28 |
+
|
| 29 |
+
Stage 11 is the unique solution. No empty `(puzzle, stage)` pair.
|
| 30 |
+
|
| 31 |
+
### Files (`data/`)
|
| 32 |
+
|
| 33 |
+
| file | shape | dtype | role |
|
| 34 |
+
| --- | --- | --- | --- |
|
| 35 |
+
| `{split}_assignments.npy` | `(M, 81)` | uint8 | one full board per instance, cell `r*9+c` |
|
| 36 |
+
| `{split}_starts.npy` | `(N, 12)` | int32 | first row in `assignments` for `(puzzle, stage)` |
|
| 37 |
+
| `{split}_counts.npy` | `(N, 12)` | uint8 | number of instances for `(puzzle, stage)` |
|
| 38 |
+
| `{split}_index.npy` | `(M, 3)` | int32 | `[puzzle_idx, stage, k]` (optional; starts/counts are enough) |
|
| 39 |
+
|
| 40 |
+
The trainer only needs `assignments`, `starts`, and `counts`.
|
| 41 |
+
|
| 42 |
+
Puzzle clue/solution arrays are **not** in this repo (they are the
|
| 43 |
+
original Sudoku npy files). Candidate masks used to *build* the
|
| 44 |
+
instances live in `datasets_multicandidate_s12/`.
|
| 45 |
+
|
| 46 |
+
### How a training example is built
|
| 47 |
+
|
| 48 |
+
1. Take the usual solver-order sequence: clue triples, then K latent
|
| 49 |
+
placeholders, then empty-cell triples.
|
| 50 |
+
2. Sample one instance for the current curriculum stage.
|
| 51 |
+
3. Rewrite **values only**. Location order stays solver-order.
|
| 52 |
+
4. Predict the output triples with softmax CE. Latent slots are not
|
| 53 |
+
predicted.
|
| 54 |
+
|
| 55 |
+
Curriculum stage `t` (1..12) trains on stage-`(t-1)` instances.
|
| 56 |
+
Stage 12 targets the unique solution.
|
| 57 |
+
|
| 58 |
+
## Code (`code/`)
|
| 59 |
+
|
| 60 |
+
- `code/train/` — JAX trainer (`data.py`, `trainer.py`, `evaluater.py`,
|
| 61 |
+
`train_and_evaluate.py`, `train_backtrack.py`, `main.py`, `model.py`)
|
| 62 |
+
- `code/build_superposition_dataset.py` — instance generator
|
| 63 |
+
- `code/build_instance_offsets.py` — `starts` / `counts` tables
|
| 64 |
+
- `code/superposition_instances.py` — per-puzzle instance sampler
|
| 65 |
+
- `code/sbatch_instance_latent.sh` — Slurm launch (feanor / H200)
|
| 66 |
+
|
| 67 |
+
Set `SUDOKU_INSTANCE_DIR` to the `data/` folder (or a local copy).
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
from huggingface_hub import snapshot_download
|
| 71 |
+
snapshot_download("Avra98/Sudoku_superposition", local_dir="Sudoku_superposition")
|
| 72 |
+
```
|
code/build_instance_offsets.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Precompute (puzzle, stage) -> row range into the instance assignments.
|
| 2 |
+
|
| 3 |
+
The index rows are grouped by puzzle then stage, so a start offset plus a count
|
| 4 |
+
per (puzzle, stage) is enough for O(1) sampling in the data loader.
|
| 5 |
+
|
| 6 |
+
Writes {split}_starts.npy (N, S) int32 and {split}_counts.npy (N, S) uint8.
|
| 7 |
+
"""
|
| 8 |
+
import argparse
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
ap = argparse.ArgumentParser()
|
| 16 |
+
ap.add_argument("--dir", default="datasets_superposition")
|
| 17 |
+
ap.add_argument("--splits", default="train,test")
|
| 18 |
+
ap.add_argument("--stages", type=int, default=12)
|
| 19 |
+
args = ap.parse_args()
|
| 20 |
+
|
| 21 |
+
S = args.stages
|
| 22 |
+
for split in args.splits.split(","):
|
| 23 |
+
split = split.strip()
|
| 24 |
+
ix = np.load(os.path.join(args.dir, f"{split}_index.npy"), mmap_mode="r")
|
| 25 |
+
n_puzzles = int(np.array(ix[-1, 0])) + 1
|
| 26 |
+
key = np.array(ix[:, 0]).astype(np.int64) * S + np.array(ix[:, 1])
|
| 27 |
+
counts = np.bincount(key, minlength=n_puzzles * S).astype(np.int64)
|
| 28 |
+
if counts.max() > 255:
|
| 29 |
+
raise ValueError(f"count {counts.max()} exceeds uint8")
|
| 30 |
+
starts = np.concatenate([[0], np.cumsum(counts)[:-1]]).astype(np.int64)
|
| 31 |
+
|
| 32 |
+
starts = starts.reshape(n_puzzles, S).astype(np.int32)
|
| 33 |
+
counts = counts.reshape(n_puzzles, S).astype(np.uint8)
|
| 34 |
+
|
| 35 |
+
np.save(os.path.join(args.dir, f"{split}_starts.npy"), starts)
|
| 36 |
+
np.save(os.path.join(args.dir, f"{split}_counts.npy"), counts)
|
| 37 |
+
|
| 38 |
+
empty = int((counts == 0).sum())
|
| 39 |
+
print(f"[{split}] puzzles={n_puzzles:,} rows={len(ix):,} "
|
| 40 |
+
f"(puzzle,stage) cells with zero instances: {empty:,}")
|
| 41 |
+
print(f" mean instances per stage: "
|
| 42 |
+
+ " ".join(f"{counts[:, s].mean():.2f}" for s in range(S)))
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
main()
|
code/build_superposition_dataset.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Materialize superposition instances for every puzzle and every stage.
|
| 2 |
+
|
| 3 |
+
Input: datasets_multicandidate_s12/{split}_cand_masks.npy (N, 12, 81)
|
| 4 |
+
Output: datasets_superposition/{split}_assignments.npy (M, 81) uint8
|
| 5 |
+
datasets_superposition/{split}_index.npy (M, 3) int32
|
| 6 |
+
columns: [puzzle_idx, stage, instance_within_stage]
|
| 7 |
+
|
| 8 |
+
M is about 48 x N (one puzzle expands into ~6 instances at stage 0 down to 1
|
| 9 |
+
at stage 11). Generation is embarrassingly parallel over puzzles.
|
| 10 |
+
"""
|
| 11 |
+
import argparse
|
| 12 |
+
import os
|
| 13 |
+
import time
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import multiprocessing as mp
|
| 17 |
+
|
| 18 |
+
import superposition_instances as SI
|
| 19 |
+
|
| 20 |
+
_MASKS = None
|
| 21 |
+
_ARGS = None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _init(path, args):
|
| 25 |
+
global _MASKS, _ARGS
|
| 26 |
+
_MASKS = np.load(path, mmap_mode="r")
|
| 27 |
+
_ARGS = args
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _one(idx):
|
| 31 |
+
assigns, index = [], []
|
| 32 |
+
for s in range(_MASKS.shape[1]):
|
| 33 |
+
mask = np.array(_MASKS[idx, s]).astype(np.uint16)
|
| 34 |
+
r = SI.instances_for_stage(
|
| 35 |
+
mask,
|
| 36 |
+
max_confine=_ARGS.max_confine,
|
| 37 |
+
max_instances=_ARGS.max_instances,
|
| 38 |
+
max_attempts=_ARGS.max_attempts,
|
| 39 |
+
seed=idx * 100 + s,
|
| 40 |
+
max_repair=_ARGS.max_repair,
|
| 41 |
+
require_new=True,
|
| 42 |
+
patience=_ARGS.patience,
|
| 43 |
+
)
|
| 44 |
+
inst = r["instances"]
|
| 45 |
+
if len(inst) == 0:
|
| 46 |
+
continue
|
| 47 |
+
assigns.append(inst.astype(np.uint8))
|
| 48 |
+
for k in range(len(inst)):
|
| 49 |
+
index.append((idx, s, k))
|
| 50 |
+
if not assigns:
|
| 51 |
+
return (np.zeros((0, 81), dtype=np.uint8),
|
| 52 |
+
np.zeros((0, 3), dtype=np.int32))
|
| 53 |
+
return (np.concatenate(assigns, axis=0),
|
| 54 |
+
np.array(index, dtype=np.int32))
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def process_split(split, args):
|
| 58 |
+
mask_path = os.path.join(args.mask_dir, f"{split}_cand_masks.npy")
|
| 59 |
+
masks = np.load(mask_path, mmap_mode="r")
|
| 60 |
+
n = len(masks) if args.limit is None else min(args.limit, len(masks))
|
| 61 |
+
print(f"[{split}] {n:,} puzzles from {mask_path}", flush=True)
|
| 62 |
+
|
| 63 |
+
t0 = time.time()
|
| 64 |
+
chunks_a, chunks_i = [], []
|
| 65 |
+
done = 0
|
| 66 |
+
with mp.Pool(args.workers, initializer=_init,
|
| 67 |
+
initargs=(mask_path, args)) as pool:
|
| 68 |
+
for a, i in pool.imap(_one, range(n), chunksize=8):
|
| 69 |
+
if len(a):
|
| 70 |
+
chunks_a.append(a)
|
| 71 |
+
chunks_i.append(i)
|
| 72 |
+
done += 1
|
| 73 |
+
if done % 2000 == 0 or done == n:
|
| 74 |
+
rate = done / max(time.time() - t0, 1e-6)
|
| 75 |
+
kept = sum(len(x) for x in chunks_i)
|
| 76 |
+
print(f" {done:,}/{n:,} {rate:.0f} puzzles/s "
|
| 77 |
+
f"{kept:,} instances "
|
| 78 |
+
f"({kept / done:.1f} per puzzle)", flush=True)
|
| 79 |
+
|
| 80 |
+
assignments = (np.concatenate(chunks_a, axis=0) if chunks_a
|
| 81 |
+
else np.zeros((0, 81), dtype=np.uint8))
|
| 82 |
+
index = (np.concatenate(chunks_i, axis=0) if chunks_i
|
| 83 |
+
else np.zeros((0, 3), dtype=np.int32))
|
| 84 |
+
|
| 85 |
+
os.makedirs(args.out_dir, exist_ok=True)
|
| 86 |
+
ap = os.path.join(args.out_dir, f"{split}_assignments.npy")
|
| 87 |
+
ip = os.path.join(args.out_dir, f"{split}_index.npy")
|
| 88 |
+
np.save(ap, assignments)
|
| 89 |
+
np.save(ip, index)
|
| 90 |
+
|
| 91 |
+
elapsed = time.time() - t0
|
| 92 |
+
print(f"[{split}] saved {len(assignments):,} instances "
|
| 93 |
+
f"({len(assignments) / n:.2f} per puzzle) in {elapsed / 60:.1f} min",
|
| 94 |
+
flush=True)
|
| 95 |
+
print(f" {ap} {os.path.getsize(ap) / 1e9:.2f} GB", flush=True)
|
| 96 |
+
print(f" {ip} {os.path.getsize(ip) / 1e9:.2f} GB", flush=True)
|
| 97 |
+
|
| 98 |
+
# per-stage instance counts
|
| 99 |
+
print(f"[{split}] instances per stage:", flush=True)
|
| 100 |
+
for s in range(masks.shape[1]):
|
| 101 |
+
c = int((index[:, 1] == s).sum())
|
| 102 |
+
print(f" stage {s:>2}: {c:>12,} ({c / n:.2f} per puzzle)",
|
| 103 |
+
flush=True)
|
| 104 |
+
return len(assignments)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def main():
|
| 108 |
+
ap = argparse.ArgumentParser()
|
| 109 |
+
ap.add_argument("--mask_dir", default="datasets_multicandidate_s12")
|
| 110 |
+
ap.add_argument("--out_dir", default="datasets_superposition")
|
| 111 |
+
ap.add_argument("--splits", default="train,test")
|
| 112 |
+
ap.add_argument("--limit", type=int, default=None)
|
| 113 |
+
ap.add_argument("--max-confine", type=int, default=1)
|
| 114 |
+
ap.add_argument("--max-instances", type=int, default=32)
|
| 115 |
+
ap.add_argument("--max-attempts", type=int, default=400)
|
| 116 |
+
ap.add_argument("--max-repair", type=int, default=80)
|
| 117 |
+
ap.add_argument("--patience", type=int, default=40)
|
| 118 |
+
ap.add_argument("--workers", type=int, default=64)
|
| 119 |
+
args = ap.parse_args()
|
| 120 |
+
|
| 121 |
+
print(f"confine |S|<={args.max_confine} workers={args.workers} "
|
| 122 |
+
f"out={args.out_dir}", flush=True)
|
| 123 |
+
for split in args.splits.split(","):
|
| 124 |
+
process_split(split.strip(), args)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
if __name__ == "__main__":
|
| 128 |
+
main()
|
code/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/launch_instance_latent.sh
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# 12-stage latent curriculum, instance-wise superposition targets.
|
| 3 |
+
#
|
| 4 |
+
# Same architecture and curriculum as w12_latent, except:
|
| 5 |
+
# - the output prompt is one sampled stage-k assignment, not the unique
|
| 6 |
+
# solution (input clues stay fixed)
|
| 7 |
+
# - the candidate-head BCE is off (SUDOKU_AUX_WEIGHT=0)
|
| 8 |
+
# - promotion is gated on the in-set rate (emitted digit is a stage-k
|
| 9 |
+
# candidate), not on candidate-set exact match
|
| 10 |
+
#
|
| 11 |
+
# Usage:
|
| 12 |
+
# bash launch_instance_latent.sh # GPU 0, full 800k run
|
| 13 |
+
# GPU=6 bash launch_instance_latent.sh # pick another GPU
|
| 14 |
+
# SUDOKU_MAX_STEPS=200 bash launch_instance_latent.sh # smoke
|
| 15 |
+
|
| 16 |
+
set -u
|
| 17 |
+
|
| 18 |
+
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 19 |
+
_CODE_DIR="$(cd "${_SCRIPT_DIR}/.." && pwd)"
|
| 20 |
+
export SUDOKU_RUN_DIR="${SUDOKU_RUN_DIR:-${_SCRIPT_DIR}}"
|
| 21 |
+
# shellcheck source=env_paths.sh
|
| 22 |
+
source "${_SCRIPT_DIR}/env_paths.sh"
|
| 23 |
+
|
| 24 |
+
cd "$RUN_DIR" || exit 1
|
| 25 |
+
if [ -n "${CUDNN_LIB}" ]; then export LD_LIBRARY_PATH="${CUDNN_LIB}:${LD_LIBRARY_PATH:-}"; fi
|
| 26 |
+
export XLA_PYTHON_CLIENT_MEM_FRACTION=0.9
|
| 27 |
+
|
| 28 |
+
GPU="${GPU:-0}"
|
| 29 |
+
name="${NAME:-w12_inst_latent}"
|
| 30 |
+
mkdir -p logs
|
| 31 |
+
|
| 32 |
+
export SUDOKU_TRAIN_PATH="${_CODE_DIR}/datasets/train_sudoku_puzzles.npy"
|
| 33 |
+
export SUDOKU_TEST_PATH="${_CODE_DIR}/datasets/test_sudoku_puzzles.npy"
|
| 34 |
+
# Masks are a metric only (in-set rate / promotion). Not a training target.
|
| 35 |
+
export SUDOKU_TRAIN_CAND="${_CODE_DIR}/datasets_multicandidate_s12/train_cand_masks.npy"
|
| 36 |
+
export SUDOKU_TEST_CAND="${_CODE_DIR}/datasets_multicandidate_s12/test_cand_masks.npy"
|
| 37 |
+
export SUDOKU_INSTANCE_DIR="${_CODE_DIR}/datasets_superposition"
|
| 38 |
+
|
| 39 |
+
export SUDOKU_RESUME=0
|
| 40 |
+
export SUDOKU_LATENT_SLOTS=12
|
| 41 |
+
export SUDOKU_RECURRENT=1
|
| 42 |
+
export SUDOKU_BACKTRACK=0
|
| 43 |
+
export SUDOKU_START_STAGE=1
|
| 44 |
+
export SUDOKU_MAX_STAGE=12
|
| 45 |
+
export SUDOKU_CAND_SLOT_MODE=depth
|
| 46 |
+
export SUDOKU_PASSES_PER_STAGE=1
|
| 47 |
+
export SUDOKU_AUX_WEIGHT=0.0
|
| 48 |
+
export SUDOKU_LEVEL_BALANCED=0
|
| 49 |
+
export SUDOKU_DATA_CURRICULUM=none
|
| 50 |
+
|
| 51 |
+
export SUDOKU_PLATEAU_STEPS="${SUDOKU_PLATEAU_STEPS:-20000}"
|
| 52 |
+
export SUDOKU_PLATEAU_DELTA=0.005
|
| 53 |
+
export SUDOKU_PATIENCE="${SUDOKU_PATIENCE:-80000}"
|
| 54 |
+
export SUDOKU_MIN_STAGE_STEPS="${SUDOKU_MIN_STAGE_STEPS:-8000}"
|
| 55 |
+
export SUDOKU_PROMOTE_ACC="${SUDOKU_PROMOTE_ACC:-0.90}"
|
| 56 |
+
export SUDOKU_MAX_STEPS="${SUDOKU_MAX_STEPS:-800000}"
|
| 57 |
+
export SUDOKU_EVAL_EVERY="${SUDOKU_EVAL_EVERY:-2000}"
|
| 58 |
+
export SUDOKU_SAVE_EVERY="${SUDOKU_SAVE_EVERY:-10000}"
|
| 59 |
+
export SUDOKU_CKPT_KEEP=100
|
| 60 |
+
|
| 61 |
+
export SUDOKU_LR=0.0002
|
| 62 |
+
export SUDOKU_DROPOUT=0.2
|
| 63 |
+
export SUDOKU_WD=0.005
|
| 64 |
+
|
| 65 |
+
echo "launching ${name} on GPU ${GPU}"
|
| 66 |
+
echo " K=12 recurrent=1 aux=0 instance_dir=${SUDOKU_INSTANCE_DIR}"
|
| 67 |
+
echo " max_steps=${SUDOKU_MAX_STEPS} plateau=${SUDOKU_PLATEAU_STEPS} patience=${SUDOKU_PATIENCE}"
|
| 68 |
+
CUDA_VISIBLE_DEVICES="${GPU}" \
|
| 69 |
+
nohup "$PY" -u -m train.main \
|
| 70 |
+
--workdir="./logs/${name}" \
|
| 71 |
+
--exp_name="${name}" \
|
| 72 |
+
> "logs/${name}.log" 2>&1 &
|
| 73 |
+
echo " pid $! log=${RUN_DIR}/logs/${name}.log"
|
code/level_instance_stats.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Candidate-set size and instance count per stage, level 3 vs level 8.
|
| 2 |
+
|
| 3 |
+
Two metrics only, measured on the training masks.
|
| 4 |
+
"""
|
| 5 |
+
import argparse
|
| 6 |
+
import multiprocessing as mp
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
import superposition_instances as SI
|
| 11 |
+
|
| 12 |
+
_MASKS = None
|
| 13 |
+
_ARGS = None
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _init(path, args):
|
| 17 |
+
global _MASKS, _ARGS
|
| 18 |
+
_MASKS = np.load(path, mmap_mode="r")
|
| 19 |
+
_ARGS = args
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _one(idx):
|
| 23 |
+
out = []
|
| 24 |
+
for s in range(_MASKS.shape[1]):
|
| 25 |
+
mask = np.array(_MASKS[idx, s]).astype(np.uint16)
|
| 26 |
+
_, empties = SI.split_cells(mask)
|
| 27 |
+
sizes = [len(SI.digits_of(mask[c])) for c in empties]
|
| 28 |
+
width = float(np.mean(sizes)) if sizes else 1.0
|
| 29 |
+
wmax = int(np.max(sizes)) if sizes else 1
|
| 30 |
+
r = SI.instances_for_stage(
|
| 31 |
+
mask, max_confine=_ARGS.max_confine,
|
| 32 |
+
max_instances=_ARGS.max_instances,
|
| 33 |
+
max_attempts=_ARGS.max_attempts, seed=idx * 100 + s,
|
| 34 |
+
max_repair=_ARGS.max_repair)
|
| 35 |
+
out.append((s, width, wmax, r["n_instances"], len(empties)))
|
| 36 |
+
return out
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def run(level, args):
|
| 40 |
+
meta = np.load(args.meta)
|
| 41 |
+
idxs = np.where(meta[:, 1] == level)[0][:args.limit].tolist()
|
| 42 |
+
with mp.Pool(args.workers, initializer=_init,
|
| 43 |
+
initargs=(args.masks, args)) as pool:
|
| 44 |
+
res = pool.map(_one, idxs, chunksize=4)
|
| 45 |
+
widths, maxes, counts, opens = {}, {}, {}, {}
|
| 46 |
+
for rows in res:
|
| 47 |
+
for s, w, wm, n, ne in rows:
|
| 48 |
+
widths.setdefault(s, []).append(w)
|
| 49 |
+
maxes.setdefault(s, []).append(wm)
|
| 50 |
+
counts.setdefault(s, []).append(n)
|
| 51 |
+
opens.setdefault(s, []).append(ne)
|
| 52 |
+
return len(idxs), widths, maxes, counts, opens
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def main():
|
| 56 |
+
ap = argparse.ArgumentParser()
|
| 57 |
+
ap.add_argument("--masks", default="datasets_multicandidate_s12/train_cand_masks.npy")
|
| 58 |
+
ap.add_argument("--meta", default="datasets_multicandidate_s12/train_meta.npy")
|
| 59 |
+
ap.add_argument("--limit", type=int, default=400)
|
| 60 |
+
ap.add_argument("--max-confine", type=int, default=1)
|
| 61 |
+
ap.add_argument("--max-instances", type=int, default=64)
|
| 62 |
+
ap.add_argument("--max-attempts", type=int, default=300)
|
| 63 |
+
ap.add_argument("--max-repair", type=int, default=120)
|
| 64 |
+
ap.add_argument("--workers", type=int, default=60)
|
| 65 |
+
args = ap.parse_args()
|
| 66 |
+
|
| 67 |
+
n3, w3, m3, c3, o3 = run(3, args)
|
| 68 |
+
n8, w8, m8, c8, o8 = run(8, args)
|
| 69 |
+
|
| 70 |
+
print(f"training masks, {n3} level-3 puzzles and {n8} level-8 puzzles")
|
| 71 |
+
print(f"confinement threshold |S| <= {args.max_confine}")
|
| 72 |
+
print()
|
| 73 |
+
print(f"{'':>5} {'------------- level 3 -------------':>42} "
|
| 74 |
+
f"{'------------- level 8 -------------':>42}")
|
| 75 |
+
print(f"{'stage':>5} {'mean':>7} {'max':>7} {'worst':>7} {'open':>7} "
|
| 76 |
+
f"{'inst':>7} {'mean':>7} {'max':>7} {'worst':>7} {'open':>7} {'inst':>7}")
|
| 77 |
+
print("-" * 96)
|
| 78 |
+
for s in sorted(w3):
|
| 79 |
+
print(f"{s:>5} "
|
| 80 |
+
f"{np.mean(w3[s]):>7.2f} {np.mean(m3[s]):>7.2f} "
|
| 81 |
+
f"{np.max(m3[s]):>7d} {np.mean(o3[s]):>7.1f} "
|
| 82 |
+
f"{np.mean(c3[s]):>7.1f} "
|
| 83 |
+
f"{np.mean(w8[s]):>7.2f} {np.mean(m8[s]):>7.2f} "
|
| 84 |
+
f"{np.max(m8[s]):>7d} {np.mean(o8[s]):>7.1f} "
|
| 85 |
+
f"{np.mean(c8[s]):>7.1f}")
|
| 86 |
+
print()
|
| 87 |
+
print("mean = mean candidate set size over undetermined cells")
|
| 88 |
+
print("max = mean over puzzles of the largest candidate set in that puzzle")
|
| 89 |
+
print("worst = largest candidate set seen in any puzzle at that stage")
|
| 90 |
+
print("open = undetermined cells remaining; inst = instances at saturation")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
if __name__ == "__main__":
|
| 94 |
+
main()
|
code/sbatch_instance_latent.sh
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 35 |
+
SCP_OPTS="-o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
|
| 36 |
+
[ -f "${HOME}/.ssh/id_ed25519_berkeley" ] && SCP_OPTS="${SCP_OPTS} -i ${HOME}/.ssh/id_ed25519_berkeley"
|
| 37 |
+
|
| 38 |
+
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
|
| 39 |
+
echo "[$(date)] reusing ${ENV_LOCAL}"
|
| 40 |
+
else
|
| 41 |
+
echo "[$(date)] fetching env tarball"
|
| 42 |
+
rm -rf "${ENV_LOCAL}"
|
| 43 |
+
scp ${SCP_OPTS} "gandalf.berkeley.edu:${TARBALL_GANDALF}" "${TARBALL_LOCAL}"
|
| 44 |
+
tar xzf "${TARBALL_LOCAL}" -C /tmp
|
| 45 |
+
rm -f "${TARBALL_LOCAL}"
|
| 46 |
+
fi
|
| 47 |
+
|
| 48 |
+
export PY=${ENV_LOCAL}/bin/python
|
| 49 |
+
export LD_LIBRARY_PATH=\
|
| 50 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cudnn/lib:\
|
| 51 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cublas/lib:\
|
| 52 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_runtime/lib:\
|
| 53 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_nvrtc/lib:\
|
| 54 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/nccl/lib:\
|
| 55 |
+
${LD_LIBRARY_PATH:-}
|
| 56 |
+
|
| 57 |
+
${PY} -u -c "import jax; print(jax.__version__, jax.devices(), jax.default_backend())"
|
| 58 |
+
|
| 59 |
+
# ---- Stage instance files onto node-local /tmp (scratch cannot hold 8 G) ----
|
| 60 |
+
need_inst=0
|
| 61 |
+
for f in train_assignments.npy train_starts.npy train_counts.npy \
|
| 62 |
+
test_assignments.npy test_starts.npy test_counts.npy; do
|
| 63 |
+
[ -s "${INST_DIR}/${f}" ] || need_inst=1
|
| 64 |
+
done
|
| 65 |
+
if [ "${need_inst}" = 1 ]; then
|
| 66 |
+
echo "[$(date)] pulling instances from ${GANDALF_INST}"
|
| 67 |
+
mkdir -p "${INST_DIR}"
|
| 68 |
+
rsync -a --progress -e "ssh ${SCP_OPTS}" \
|
| 69 |
+
"${GANDALF_INST}/" "${INST_DIR}/"
|
| 70 |
+
fi
|
| 71 |
+
for f in train_assignments.npy train_starts.npy train_counts.npy \
|
| 72 |
+
test_assignments.npy test_starts.npy test_counts.npy; do
|
| 73 |
+
[ -s "${INST_DIR}/${f}" ] || { echo "missing ${INST_DIR}/${f}" >&2; exit 1; }
|
| 74 |
+
done
|
| 75 |
+
|
| 76 |
+
need_cand=0
|
| 77 |
+
for f in train_cand_masks.npy test_cand_masks.npy; do
|
| 78 |
+
[ -s "${CAND_DIR}/${f}" ] || need_cand=1
|
| 79 |
+
done
|
| 80 |
+
if [ "${need_cand}" = 1 ]; then
|
| 81 |
+
echo "[$(date)] pulling s12 masks from ${GANDALF_CAND}"
|
| 82 |
+
mkdir -p "${CAND_DIR}"
|
| 83 |
+
rsync -a -e "ssh ${SCP_OPTS}" "${GANDALF_CAND}/" "${CAND_DIR}/" || true
|
| 84 |
+
fi
|
| 85 |
+
for f in train_cand_masks.npy test_cand_masks.npy; do
|
| 86 |
+
[ -s "${CAND_DIR}/${f}" ] || { echo "missing ${CAND_DIR}/${f}" >&2; exit 1; }
|
| 87 |
+
done
|
| 88 |
+
|
| 89 |
+
# ---- Exact same recipe as slimgpu w12_inst_latent ----
|
| 90 |
+
export SUDOKU_RESUME=0
|
| 91 |
+
export SUDOKU_START_STAGE=1
|
| 92 |
+
export SUDOKU_MAX_STAGE=12
|
| 93 |
+
export SUDOKU_LATENT_SLOTS=12
|
| 94 |
+
export SUDOKU_RECURRENT=1
|
| 95 |
+
export SUDOKU_BACKTRACK=0
|
| 96 |
+
export SUDOKU_CAND_SLOT_MODE=depth
|
| 97 |
+
export SUDOKU_PASSES_PER_STAGE=1
|
| 98 |
+
export SUDOKU_AUX_WEIGHT=0.0
|
| 99 |
+
export SUDOKU_LEVEL_BALANCED=0
|
| 100 |
+
export SUDOKU_DATA_CURRICULUM=none
|
| 101 |
+
export SUDOKU_PLATEAU_STEPS=20000
|
| 102 |
+
export SUDOKU_PLATEAU_DELTA=0.005
|
| 103 |
+
export SUDOKU_PATIENCE=80000
|
| 104 |
+
export SUDOKU_MIN_STAGE_STEPS=8000
|
| 105 |
+
# Stage 1->2: inset >= 0.85 (chance is ~0.41 on stage-0 sets) AND
|
| 106 |
+
# loc_acc >= 0.70. Frontier is min(inset, loc), so both must clear 0.85
|
| 107 |
+
# unless loc is the limiter (then 0.85 loc is required). The loc floor
|
| 108 |
+
# is applied via min(); SUDOKU_PROMOTE_LOC is the documented intent.
|
| 109 |
+
export SUDOKU_PROMOTE_ACC=0.85
|
| 110 |
+
export SUDOKU_PROMOTE_LOC=0.70
|
| 111 |
+
export SUDOKU_MAX_STEPS="${SUDOKU_MAX_STEPS:-800000}"
|
| 112 |
+
export SUDOKU_EVAL_EVERY=2000
|
| 113 |
+
export SUDOKU_SAVE_EVERY=10000
|
| 114 |
+
export SUDOKU_CKPT_KEEP=3
|
| 115 |
+
export SUDOKU_LR=0.0002
|
| 116 |
+
export SUDOKU_DROPOUT=0.2
|
| 117 |
+
export SUDOKU_WD=0.005
|
| 118 |
+
export SUDOKU_TRAIN_PATH="${SCRATCH_ROOT}/sudoku-code/datasets/train_sudoku_puzzles.npy"
|
| 119 |
+
export SUDOKU_TEST_PATH="${SCRATCH_ROOT}/sudoku-code/datasets/test_sudoku_puzzles.npy"
|
| 120 |
+
export SUDOKU_TRAIN_CAND="${CAND_DIR}/train_cand_masks.npy"
|
| 121 |
+
export SUDOKU_TEST_CAND="${CAND_DIR}/test_cand_masks.npy"
|
| 122 |
+
export SUDOKU_INSTANCE_DIR="${INST_DIR}"
|
| 123 |
+
export XLA_PYTHON_CLIENT_MEM_FRACTION=0.9
|
| 124 |
+
|
| 125 |
+
cd "${RUN_DIR}"
|
| 126 |
+
(
|
| 127 |
+
while true; do sleep 900
|
| 128 |
+
rsync -a "${LOCAL_LOG}.log" "${RUN_DIR}/logs/w12_inst_latent.log" 2>/dev/null || true
|
| 129 |
+
done
|
| 130 |
+
) &
|
| 131 |
+
SYNC_PID=$!
|
| 132 |
+
|
| 133 |
+
echo "[$(date)] starting w12_inst_latent from scratch"
|
| 134 |
+
echo " K=12 recurrent=1 bt=0 aux=0 instance_dir=${INST_DIR}"
|
| 135 |
+
CUDA_VISIBLE_DEVICES=0 ${PY} -u -m train.main \
|
| 136 |
+
--workdir="${LOCAL_LOG}" --exp_name="w12_inst_latent" \
|
| 137 |
+
> "${LOCAL_LOG}.log" 2>&1
|
| 138 |
+
EC=$?
|
| 139 |
+
kill ${SYNC_PID} 2>/dev/null || true
|
| 140 |
+
rsync -a "${LOCAL_LOG}.log" "${RUN_DIR}/logs/w12_inst_latent.log" 2>/dev/null || true
|
| 141 |
+
echo "[$(date)] w12_inst_latent exit ${EC}"
|
| 142 |
+
exit ${EC}
|
code/sbatch_instance_latent_bt.sh
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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_instbt
|
| 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 instance-CE latent curriculum as w12_inst, plus adaptive backtrack:
|
| 13 |
+
# if a graduated stage's in-set rate falls more than the margin below its
|
| 14 |
+
# graduation value, train at that stage's depth until it recovers.
|
| 15 |
+
|
| 16 |
+
set -u
|
| 17 |
+
hostname
|
| 18 |
+
nvidia-smi -L
|
| 19 |
+
echo "[$(date)] w12_inst_latent_bt"
|
| 20 |
+
|
| 21 |
+
SCRATCH_ROOT=/scratch/users/gatmiry/llm-reasoning-logic-puzzles
|
| 22 |
+
RUN_DIR=${SCRATCH_ROOT}/sudoku-code/wavecurriculum_run
|
| 23 |
+
ENV_LOCAL=/tmp/logicpuzzles
|
| 24 |
+
CAND_DIR=/tmp/sudoku_s12
|
| 25 |
+
INST_DIR=/tmp/sudoku_superposition
|
| 26 |
+
LOCAL_LOG=/tmp/sudoku_wave_runs/w12_inst_latent_bt
|
| 27 |
+
TARBALL_GANDALF=/tmp/logicpuzzles_env.tar.gz
|
| 28 |
+
TARBALL_LOCAL=/tmp/logicpuzzles_env_${SLURM_JOB_ID}.tar.gz
|
| 29 |
+
GANDALF_INST=gandalf.berkeley.edu:/tmp/sudoku_superposition
|
| 30 |
+
GANDALF_CAND=gandalf.berkeley.edu:/tmp/sudoku_s12
|
| 31 |
+
|
| 32 |
+
mkdir -p "${RUN_DIR}/logs" "${LOCAL_LOG}" /tmp/sudoku_wave_runs
|
| 33 |
+
|
| 34 |
+
SCP_OPTS="-o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
|
| 35 |
+
[ -f "${HOME}/.ssh/id_ed25519_berkeley" ] && SCP_OPTS="${SCP_OPTS} -i ${HOME}/.ssh/id_ed25519_berkeley"
|
| 36 |
+
|
| 37 |
+
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
|
| 38 |
+
echo "[$(date)] reusing ${ENV_LOCAL}"
|
| 39 |
+
else
|
| 40 |
+
echo "[$(date)] fetching env tarball"
|
| 41 |
+
rm -rf "${ENV_LOCAL}"
|
| 42 |
+
scp ${SCP_OPTS} "gandalf.berkeley.edu:${TARBALL_GANDALF}" "${TARBALL_LOCAL}"
|
| 43 |
+
tar xzf "${TARBALL_LOCAL}" -C /tmp
|
| 44 |
+
rm -f "${TARBALL_LOCAL}"
|
| 45 |
+
fi
|
| 46 |
+
|
| 47 |
+
export PY=${ENV_LOCAL}/bin/python
|
| 48 |
+
export LD_LIBRARY_PATH=\
|
| 49 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cudnn/lib:\
|
| 50 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cublas/lib:\
|
| 51 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_runtime/lib:\
|
| 52 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/cuda_nvrtc/lib:\
|
| 53 |
+
${ENV_LOCAL}/lib/python3.9/site-packages/nvidia/nccl/lib:\
|
| 54 |
+
${LD_LIBRARY_PATH:-}
|
| 55 |
+
|
| 56 |
+
${PY} -u -c "import jax; print(jax.__version__, jax.devices(), jax.default_backend())"
|
| 57 |
+
|
| 58 |
+
need_inst=0
|
| 59 |
+
for f in train_assignments.npy train_starts.npy train_counts.npy \
|
| 60 |
+
test_assignments.npy test_starts.npy test_counts.npy; do
|
| 61 |
+
[ -s "${INST_DIR}/${f}" ] || need_inst=1
|
| 62 |
+
done
|
| 63 |
+
if [ "${need_inst}" = 1 ]; then
|
| 64 |
+
echo "[$(date)] pulling instances from ${GANDALF_INST}"
|
| 65 |
+
mkdir -p "${INST_DIR}"
|
| 66 |
+
rsync -a --progress -e "ssh ${SCP_OPTS}" \
|
| 67 |
+
"${GANDALF_INST}/" "${INST_DIR}/"
|
| 68 |
+
fi
|
| 69 |
+
for f in train_assignments.npy train_starts.npy train_counts.npy \
|
| 70 |
+
test_assignments.npy test_starts.npy test_counts.npy; do
|
| 71 |
+
[ -s "${INST_DIR}/${f}" ] || { echo "missing ${INST_DIR}/${f}" >&2; exit 1; }
|
| 72 |
+
done
|
| 73 |
+
|
| 74 |
+
need_cand=0
|
| 75 |
+
for f in train_cand_masks.npy test_cand_masks.npy; do
|
| 76 |
+
[ -s "${CAND_DIR}/${f}" ] || need_cand=1
|
| 77 |
+
done
|
| 78 |
+
if [ "${need_cand}" = 1 ]; then
|
| 79 |
+
echo "[$(date)] pulling s12 masks from ${GANDALF_CAND}"
|
| 80 |
+
mkdir -p "${CAND_DIR}"
|
| 81 |
+
rsync -a -e "ssh ${SCP_OPTS}" "${GANDALF_CAND}/" "${CAND_DIR}/" || true
|
| 82 |
+
fi
|
| 83 |
+
for f in train_cand_masks.npy test_cand_masks.npy; do
|
| 84 |
+
[ -s "${CAND_DIR}/${f}" ] || { echo "missing ${CAND_DIR}/${f}" >&2; exit 1; }
|
| 85 |
+
done
|
| 86 |
+
|
| 87 |
+
export SUDOKU_RESUME=0
|
| 88 |
+
export SUDOKU_START_STAGE=1
|
| 89 |
+
export SUDOKU_MAX_STAGE=12
|
| 90 |
+
export SUDOKU_LATENT_SLOTS=12
|
| 91 |
+
export SUDOKU_RECURRENT=1
|
| 92 |
+
export SUDOKU_CAND_SLOT_MODE=depth
|
| 93 |
+
export SUDOKU_PASSES_PER_STAGE=1
|
| 94 |
+
export SUDOKU_AUX_WEIGHT=0.0
|
| 95 |
+
export SUDOKU_LEVEL_BALANCED=0
|
| 96 |
+
export SUDOKU_DATA_CURRICULUM=none
|
| 97 |
+
export SUDOKU_PLATEAU_STEPS=20000
|
| 98 |
+
export SUDOKU_PLATEAU_DELTA=0.005
|
| 99 |
+
export SUDOKU_PATIENCE=80000
|
| 100 |
+
export SUDOKU_MIN_STAGE_STEPS=8000
|
| 101 |
+
export SUDOKU_PROMOTE_ACC=0.85
|
| 102 |
+
export SUDOKU_PROMOTE_LOC=0.70
|
| 103 |
+
export SUDOKU_MAX_STEPS="${SUDOKU_MAX_STEPS:-800000}"
|
| 104 |
+
export SUDOKU_EVAL_EVERY=2000
|
| 105 |
+
export SUDOKU_SAVE_EVERY=10000
|
| 106 |
+
export SUDOKU_CKPT_KEEP=3
|
| 107 |
+
export SUDOKU_LR=0.0002
|
| 108 |
+
export SUDOKU_DROPOUT=0.2
|
| 109 |
+
export SUDOKU_WD=0.005
|
| 110 |
+
export SUDOKU_TRAIN_PATH="${SCRATCH_ROOT}/sudoku-code/datasets/train_sudoku_puzzles.npy"
|
| 111 |
+
export SUDOKU_TEST_PATH="${SCRATCH_ROOT}/sudoku-code/datasets/test_sudoku_puzzles.npy"
|
| 112 |
+
export SUDOKU_TRAIN_CAND="${CAND_DIR}/train_cand_masks.npy"
|
| 113 |
+
export SUDOKU_TEST_CAND="${CAND_DIR}/test_cand_masks.npy"
|
| 114 |
+
export SUDOKU_INSTANCE_DIR="${INST_DIR}"
|
| 115 |
+
export XLA_PYTHON_CLIENT_MEM_FRACTION=0.9
|
| 116 |
+
|
| 117 |
+
# Adaptive repair: if stage t's in-set rate drops more than 0.03 below
|
| 118 |
+
# the value it graduated at, replay that depth until it recovers.
|
| 119 |
+
export SUDOKU_BACKTRACK=1
|
| 120 |
+
export SUDOKU_BACKTRACK_MODE=adaptive
|
| 121 |
+
export SUDOKU_BACKTRACK_MARGIN=0.03
|
| 122 |
+
export SUDOKU_BACKTRACK_MAX_REPAIR_STEPS=4000
|
| 123 |
+
export SUDOKU_BACKTRACK_MIN_FRONTIER_STEPS=8000
|
| 124 |
+
export SUDOKU_BACKTRACK_MAX_REPAIR_FRACTION=0.25
|
| 125 |
+
export SUDOKU_BACKTRACK_GRAD_DECAY=0.05
|
| 126 |
+
export SUDOKU_BACKTRACK_FRONTIER_MIX=1
|
| 127 |
+
|
| 128 |
+
cd "${RUN_DIR}"
|
| 129 |
+
(
|
| 130 |
+
while true; do sleep 900
|
| 131 |
+
rsync -a "${LOCAL_LOG}.log" "${RUN_DIR}/logs/w12_inst_latent_bt.log" 2>/dev/null || true
|
| 132 |
+
done
|
| 133 |
+
) &
|
| 134 |
+
SYNC_PID=$!
|
| 135 |
+
|
| 136 |
+
echo "[$(date)] starting w12_inst_latent_bt from scratch"
|
| 137 |
+
echo " K=12 recurrent=1 bt=adaptive aux=0 instance_dir=${INST_DIR}"
|
| 138 |
+
CUDA_VISIBLE_DEVICES=0 ${PY} -u -m train.main \
|
| 139 |
+
--workdir="${LOCAL_LOG}" --exp_name="w12_inst_latent_bt" \
|
| 140 |
+
> "${LOCAL_LOG}.log" 2>&1
|
| 141 |
+
EC=$?
|
| 142 |
+
kill ${SYNC_PID} 2>/dev/null || true
|
| 143 |
+
rsync -a "${LOCAL_LOG}.log" "${RUN_DIR}/logs/w12_inst_latent_bt.log" 2>/dev/null || true
|
| 144 |
+
echo "[$(date)] w12_inst_latent_bt exit ${EC}"
|
| 145 |
+
exit ${EC}
|
code/show_instance_stages.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compare what a data instance looks like at different stages, for a few cells.
|
| 2 |
+
|
| 3 |
+
Tracks the same handful of cells across stages so the shrinking candidate sets
|
| 4 |
+
and the resulting instances line up column by column.
|
| 5 |
+
"""
|
| 6 |
+
import argparse
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
import superposition_instances as SI
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def fmt(ds):
|
| 14 |
+
return "{" + ",".join(str(d) for d in ds) + "}"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def cell_name(c):
|
| 18 |
+
return f"r{c // 9}c{c % 9}"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
ap = argparse.ArgumentParser()
|
| 23 |
+
ap.add_argument("--masks", default="datasets_multicandidate_s12/test_cand_masks.npy")
|
| 24 |
+
ap.add_argument("--puzzle", type=int, default=2)
|
| 25 |
+
ap.add_argument("--stages", type=int, nargs="+", default=[1, 5, 11])
|
| 26 |
+
ap.add_argument("--ncells", type=int, default=6)
|
| 27 |
+
ap.add_argument("--show-instances", type=int, default=8)
|
| 28 |
+
ap.add_argument("--max-confine", type=int, default=3)
|
| 29 |
+
ap.add_argument("--max-instances", type=int, default=32)
|
| 30 |
+
args = ap.parse_args()
|
| 31 |
+
|
| 32 |
+
masks = np.load(args.masks, mmap_mode="r")
|
| 33 |
+
|
| 34 |
+
# Pick cells that are still undetermined at the *latest* requested stage
|
| 35 |
+
# before the final one, so the same cells are interesting at every stage.
|
| 36 |
+
pick_stage = max(s for s in args.stages if s < masks.shape[1] - 1)
|
| 37 |
+
m_pick = np.array(masks[args.puzzle, pick_stage]).astype(np.uint16)
|
| 38 |
+
_, empties = SI.split_cells(m_pick)
|
| 39 |
+
cells = sorted(empties, key=lambda c: -len(SI.digits_of(m_pick[c])))[:args.ncells]
|
| 40 |
+
cells = sorted(cells)
|
| 41 |
+
|
| 42 |
+
print(f"puzzle {args.puzzle}, tracking {len(cells)} cells: "
|
| 43 |
+
+ ", ".join(cell_name(c) for c in cells))
|
| 44 |
+
|
| 45 |
+
for s in args.stages:
|
| 46 |
+
mask = np.array(masks[args.puzzle, s]).astype(np.uint16)
|
| 47 |
+
forced, empties = SI.split_cells(mask)
|
| 48 |
+
res = SI.instances_for_stage(mask, max_confine=args.max_confine,
|
| 49 |
+
max_instances=args.max_instances, seed=7)
|
| 50 |
+
inst = res["instances"]
|
| 51 |
+
|
| 52 |
+
print()
|
| 53 |
+
print("=" * 88)
|
| 54 |
+
print(f"STAGE {s} {len(empties)} cells still undetermined, "
|
| 55 |
+
f"mean {res['mean_width']:.2f} candidates each")
|
| 56 |
+
print(f" generation: {res['n_instances']} instances kept, "
|
| 57 |
+
f"{res['n_rejected']} ruled out, "
|
| 58 |
+
f"grid-wide coverage {100*res['coverage']:.1f}%")
|
| 59 |
+
print("=" * 88)
|
| 60 |
+
|
| 61 |
+
print(" candidate set at this stage:")
|
| 62 |
+
for c in cells:
|
| 63 |
+
ds = SI.digits_of(mask[c])
|
| 64 |
+
tag = " (determined)" if len(ds) == 1 else ""
|
| 65 |
+
print(f" {cell_name(c)} {fmt(ds)}{tag}")
|
| 66 |
+
|
| 67 |
+
if not len(inst):
|
| 68 |
+
print(" no instances")
|
| 69 |
+
continue
|
| 70 |
+
|
| 71 |
+
print(f"\n first instance, written out as the (row, col, value) triples "
|
| 72 |
+
f"the model would emit:")
|
| 73 |
+
print(" " + " ".join(
|
| 74 |
+
f"({c//9},{c%9},{inst[0][c]})" for c in cells))
|
| 75 |
+
|
| 76 |
+
k = min(args.show_instances, len(inst))
|
| 77 |
+
print(f"\n value assigned to each cell, across the first {k} instances:")
|
| 78 |
+
print(" inst " + " ".join(f"{cell_name(c):>6}" for c in cells))
|
| 79 |
+
for i in range(k):
|
| 80 |
+
print(f" {i:>4} " + " ".join(
|
| 81 |
+
f"{inst[i][c]:>6}" for c in cells))
|
| 82 |
+
|
| 83 |
+
print(f"\n coverage over all {res['n_instances']} instances:")
|
| 84 |
+
for c in cells:
|
| 85 |
+
ds = set(SI.digits_of(mask[c]))
|
| 86 |
+
seen = set(int(a[c]) for a in inst)
|
| 87 |
+
miss = sorted(ds - seen)
|
| 88 |
+
status = "all candidates seen" if not miss else f"never seen: {fmt(miss)}"
|
| 89 |
+
print(f" {cell_name(c)} candidates {fmt(sorted(ds)):<16} "
|
| 90 |
+
f"seen {fmt(sorted(seen)):<16} {status}")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
if __name__ == "__main__":
|
| 94 |
+
main()
|
code/show_superposition_example.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Worked example of superposition-instance generation for one puzzle/stage.
|
| 2 |
+
|
| 3 |
+
Prints the candidate sets, the dependencies read off the mask, the instances that
|
| 4 |
+
survived, the ones that were ruled out, and the resulting per-cell coverage.
|
| 5 |
+
Display is restricted to one unit so the table is readable; generation always
|
| 6 |
+
runs on the full grid.
|
| 7 |
+
"""
|
| 8 |
+
import argparse
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
import superposition_instances as SI
|
| 13 |
+
|
| 14 |
+
NAMES = ([f"row {r}" for r in range(9)] + [f"col {c}" for c in range(9)]
|
| 15 |
+
+ [f"box {b}" for b in range(9)])
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def fmt(cells):
|
| 19 |
+
return "{" + ",".join(str(d) for d in cells) + "}"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def main():
|
| 23 |
+
ap = argparse.ArgumentParser()
|
| 24 |
+
ap.add_argument("--masks", default="datasets_multicandidate_s12/test_cand_masks.npy")
|
| 25 |
+
ap.add_argument("--puzzle", type=int, default=2)
|
| 26 |
+
ap.add_argument("--stage", type=int, default=0)
|
| 27 |
+
ap.add_argument("--unit", type=int, default=14, help="unit index to display (14 = col 5)")
|
| 28 |
+
ap.add_argument("--max-confine", type=int, default=3)
|
| 29 |
+
ap.add_argument("--max-instances", type=int, default=16)
|
| 30 |
+
args = ap.parse_args()
|
| 31 |
+
|
| 32 |
+
masks = np.load(args.masks, mmap_mode="r")
|
| 33 |
+
mask = np.array(masks[args.puzzle, args.stage]).astype(np.uint16)
|
| 34 |
+
|
| 35 |
+
unit = SI.UNITS[args.unit]
|
| 36 |
+
forced, empties = SI.split_cells(mask)
|
| 37 |
+
|
| 38 |
+
print("=" * 78)
|
| 39 |
+
print(f"puzzle {args.puzzle}, stage {args.stage}, displaying {NAMES[args.unit]}")
|
| 40 |
+
print("=" * 78)
|
| 41 |
+
print(f"grid: {len(forced)} determined cells, {len(empties)} undetermined, "
|
| 42 |
+
f"mean candidate count {np.mean([len(SI.digits_of(mask[c])) for c in empties]):.2f}")
|
| 43 |
+
|
| 44 |
+
print(f"\ncandidate sets in {NAMES[args.unit]}:")
|
| 45 |
+
for c in unit:
|
| 46 |
+
r, cc = divmod(c, 9)
|
| 47 |
+
ds = SI.digits_of(mask[c])
|
| 48 |
+
tag = "determined" if len(ds) == 1 else ""
|
| 49 |
+
print(f" r{r}c{cc} {fmt(ds):<22} {tag}")
|
| 50 |
+
|
| 51 |
+
all_deps = SI.dependencies_from_mask(mask, max_confine=9)
|
| 52 |
+
conf_deps = SI.dependencies_from_mask(mask, max_confine=args.max_confine)
|
| 53 |
+
print(f"\ndependencies over the whole grid:")
|
| 54 |
+
print(f" {len(all_deps)} total (every unit, every unplaced digit)")
|
| 55 |
+
print(f" {len(conf_deps)} with confinement size <= {args.max_confine} "
|
| 56 |
+
f"(these are the ones enforced)")
|
| 57 |
+
sizes = {}
|
| 58 |
+
for _, S in all_deps:
|
| 59 |
+
sizes[len(S)] = sizes.get(len(S), 0) + 1
|
| 60 |
+
print(" by confinement size: "
|
| 61 |
+
+ ", ".join(f"|S|={k}: {v}" for k, v in sorted(sizes.items())))
|
| 62 |
+
|
| 63 |
+
print(f"\ndependencies enforced inside {NAMES[args.unit]}:")
|
| 64 |
+
shown = 0
|
| 65 |
+
for d, S in conf_deps:
|
| 66 |
+
if not set(S) <= set(unit):
|
| 67 |
+
continue
|
| 68 |
+
locs = " or ".join(f"r{c//9}c{c%9}" for c in S)
|
| 69 |
+
print(f" digit {d} must be placed at {locs}")
|
| 70 |
+
shown += 1
|
| 71 |
+
if shown == 0:
|
| 72 |
+
print(" (none at this confinement threshold)")
|
| 73 |
+
|
| 74 |
+
res = SI.instances_for_stage(mask, max_confine=args.max_confine,
|
| 75 |
+
max_instances=args.max_instances, seed=0)
|
| 76 |
+
|
| 77 |
+
print(f"\ngeneration: {res['n_instances']} instances kept, "
|
| 78 |
+
f"{res['n_rejected']} ruled out over {res['n_attempts']} attempts")
|
| 79 |
+
print(f"coverage: {res['n_covered']}/{res['n_pairs']} "
|
| 80 |
+
f"(cell, candidate) pairs = {100*res['coverage']:.1f}%")
|
| 81 |
+
|
| 82 |
+
inst = res["instances"]
|
| 83 |
+
if len(inst):
|
| 84 |
+
print(f"\nwhat each instance assigned in {NAMES[args.unit]}:")
|
| 85 |
+
hdr = " inst " + " ".join(f"r{c//9}c{c%9}" for c in unit)
|
| 86 |
+
print(hdr)
|
| 87 |
+
for i, a in enumerate(inst):
|
| 88 |
+
print(f" {i:>4} " + " ".join(f"{a[c]:>4}" for c in unit))
|
| 89 |
+
|
| 90 |
+
print(f"\nper-cell coverage in {NAMES[args.unit]}:")
|
| 91 |
+
for c in unit:
|
| 92 |
+
ds = set(SI.digits_of(mask[c]))
|
| 93 |
+
seen = set(int(a[c]) for a in inst)
|
| 94 |
+
miss = sorted(ds - seen)
|
| 95 |
+
r, cc = divmod(c, 9)
|
| 96 |
+
status = "complete" if not miss else f"missing {fmt(miss)}"
|
| 97 |
+
print(f" r{r}c{cc} candidates {fmt(sorted(ds)):<22} "
|
| 98 |
+
f"seen {fmt(sorted(seen)):<22} {status}")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
if __name__ == "__main__":
|
| 102 |
+
main()
|
code/smoke_instance_loader.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sanity-check: instance rewrite keeps clues, varies empties, stays in-set."""
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "wavecurriculum_run"))
|
| 8 |
+
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "")
|
| 9 |
+
|
| 10 |
+
from train.data import CurriculumState, SudokuDataset # noqa: E402
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Cfg:
|
| 14 |
+
seed = 7
|
| 15 |
+
seq_order = "solver-order"
|
| 16 |
+
num_latent_slots = 12
|
| 17 |
+
latent_token_id = 10
|
| 18 |
+
curriculum_max_stage = 12
|
| 19 |
+
data_curriculum = "none"
|
| 20 |
+
cand_slot_mode = "depth"
|
| 21 |
+
passes_per_stage = 1
|
| 22 |
+
level_balanced_sampling = 0
|
| 23 |
+
train_puzzle_path = "datasets/train_sudoku_puzzles.npy"
|
| 24 |
+
test_puzzle_path = "datasets/test_sudoku_puzzles.npy"
|
| 25 |
+
train_cand_masks_path = "datasets_multicandidate_s12/train_cand_masks.npy"
|
| 26 |
+
test_cand_masks_path = "datasets_multicandidate_s12/test_cand_masks.npy"
|
| 27 |
+
instance_dir = "datasets_superposition"
|
| 28 |
+
train_meta_path = None
|
| 29 |
+
test_meta_path = None
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def main():
|
| 33 |
+
cur = CurriculumState(stage=1, max_stage=12)
|
| 34 |
+
ds = SudokuDataset(Cfg(), train=True, curriculum=cur)
|
| 35 |
+
assert ds.instances is not None
|
| 36 |
+
assert ds.cand_masks is not None
|
| 37 |
+
|
| 38 |
+
n_check = 8
|
| 39 |
+
for stage in (0, 5, 11):
|
| 40 |
+
cur.stage = stage + 1
|
| 41 |
+
clue_ok = clue_tot = 0
|
| 42 |
+
empty_in = empty_tot = empty_eq = 0
|
| 43 |
+
for idx in range(n_check):
|
| 44 |
+
base = ds.train_inputs[idx].copy()
|
| 45 |
+
si = int(ds.train_start_index[idx, 0])
|
| 46 |
+
rewritten = ds.apply_instance(base, idx, stage)
|
| 47 |
+
sol = ds.train_puzzles[idx]
|
| 48 |
+
mask = np.array(ds.cand_masks[idx, stage])
|
| 49 |
+
orig = {(int(t[0]), int(t[1])): int(t[2])
|
| 50 |
+
for t in base.reshape(81, 3)}
|
| 51 |
+
for t in rewritten[: 3 * si].reshape(-1, 3):
|
| 52 |
+
r, c, v = int(t[0]), int(t[1]), int(t[2])
|
| 53 |
+
clue_tot += 1
|
| 54 |
+
clue_ok += int(v == orig[(r, c)] == int(sol[r * 9 + c]))
|
| 55 |
+
for t in rewritten[3 * si:].reshape(-1, 3):
|
| 56 |
+
r, c, v = int(t[0]), int(t[1]), int(t[2])
|
| 57 |
+
bits = int(mask[r * 9 + c])
|
| 58 |
+
empty_tot += 1
|
| 59 |
+
empty_in += int(1 <= v <= 9 and (bits >> (v - 1)) & 1)
|
| 60 |
+
empty_eq += int(v == int(sol[r * 9 + c]))
|
| 61 |
+
print(f"stage {stage}: clues {clue_ok}/{clue_tot} "
|
| 62 |
+
f"in-set {empty_in}/{empty_tot} "
|
| 63 |
+
f"==sol {empty_eq}/{empty_tot}")
|
| 64 |
+
assert clue_ok == clue_tot, "clue values must stay the original clues"
|
| 65 |
+
assert empty_in == empty_tot, "every empty must stay in the stage set"
|
| 66 |
+
if stage == 11:
|
| 67 |
+
assert empty_eq == empty_tot, "stage 11 must be the unique solution"
|
| 68 |
+
else:
|
| 69 |
+
assert empty_eq < empty_tot, "early stages must still be ambiguous"
|
| 70 |
+
print("SMOKE OK")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
main()
|
code/superposition_instances.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build superposition instances from a stage's candidate masks.
|
| 2 |
+
|
| 3 |
+
An *instance* is one concrete assignment: exactly one digit per cell, each drawn
|
| 4 |
+
from that cell's candidate set at that stage. Across the instances for a stage,
|
| 5 |
+
every (cell, candidate) pair should appear at least once, so the candidate set is
|
| 6 |
+
recoverable from the instances without ever being supervised as a set.
|
| 7 |
+
|
| 8 |
+
Build chronology (per puzzle, per stage):
|
| 9 |
+
1. read the candidate sets from the stage mask
|
| 10 |
+
2. derive the live dependencies from that same mask
|
| 11 |
+
3. propose an instance
|
| 12 |
+
4. drop it if it disobeys any dependency
|
| 13 |
+
5. repeat 3-4 until every (cell, candidate) pair is covered
|
| 14 |
+
|
| 15 |
+
Dependencies are recomputed from the mask rather than recorded when a technique
|
| 16 |
+
fires. The stage-k mask is the cumulative product of every technique applied up
|
| 17 |
+
to stage k, so a confinement visible in the mask *is* the disjunction those
|
| 18 |
+
techniques established, and it stays visible exactly as long as it is live.
|
| 19 |
+
|
| 20 |
+
The confinement threshold matters. For a unit U and a digit d not yet placed in
|
| 21 |
+
U, S(U,d) is the set of cells of U that can still hold d, and the dependency is
|
| 22 |
+
"d must be placed somewhere in S(U,d)". Enforcing this for every unit and every
|
| 23 |
+
digit is full Sudoku unit coverage, which forces the unique solution and leaves
|
| 24 |
+
no superposition at all. Restricting to small |S(U,d)| keeps only the genuinely
|
| 25 |
+
locked disjunctions.
|
| 26 |
+
"""
|
| 27 |
+
import numpy as np
|
| 28 |
+
|
| 29 |
+
POPCOUNT = np.array([bin(i).count("1") for i in range(512)], dtype=np.int8)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def build_units():
|
| 33 |
+
"""The 27 units, each a list of 9 cell ids (cell = r*9 + c)."""
|
| 34 |
+
units = []
|
| 35 |
+
for r in range(9):
|
| 36 |
+
units.append([r * 9 + c for c in range(9)])
|
| 37 |
+
for c in range(9):
|
| 38 |
+
units.append([r * 9 + c for r in range(9)])
|
| 39 |
+
for br in range(0, 9, 3):
|
| 40 |
+
for bc in range(0, 9, 3):
|
| 41 |
+
units.append([(br + i) * 9 + (bc + j)
|
| 42 |
+
for i in range(3) for j in range(3)])
|
| 43 |
+
return units
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
UNITS = build_units()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def digits_of(m):
|
| 50 |
+
m = int(m)
|
| 51 |
+
return [d for d in range(1, 10) if m & (1 << (d - 1))]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def dependencies_from_mask(mask, max_confine=9):
|
| 55 |
+
"""Live disjunctions readable from one stage's mask.
|
| 56 |
+
|
| 57 |
+
Returns a list of (digit, cells) meaning "digit must be placed in one of
|
| 58 |
+
these cells". Only digits not already pinned in the unit are included, and
|
| 59 |
+
only when the confinement size is at most max_confine.
|
| 60 |
+
"""
|
| 61 |
+
deps = []
|
| 62 |
+
for unit in UNITS:
|
| 63 |
+
pinned = 0
|
| 64 |
+
for c in unit:
|
| 65 |
+
m = int(mask[c])
|
| 66 |
+
if m and (m & (m - 1)) == 0:
|
| 67 |
+
pinned |= m
|
| 68 |
+
for d in range(1, 10):
|
| 69 |
+
bit = 1 << (d - 1)
|
| 70 |
+
if pinned & bit:
|
| 71 |
+
continue
|
| 72 |
+
S = [c for c in unit if int(mask[c]) & bit]
|
| 73 |
+
if 1 <= len(S) <= max_confine:
|
| 74 |
+
deps.append((d, S))
|
| 75 |
+
return deps
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def split_cells(mask):
|
| 79 |
+
"""(forced cell -> digit, list of undetermined cells)."""
|
| 80 |
+
forced, empties = {}, []
|
| 81 |
+
for c in range(81):
|
| 82 |
+
m = int(mask[c])
|
| 83 |
+
if m == 0:
|
| 84 |
+
continue
|
| 85 |
+
if m & (m - 1) == 0:
|
| 86 |
+
forced[c] = m.bit_length()
|
| 87 |
+
else:
|
| 88 |
+
empties.append(c)
|
| 89 |
+
return forced, empties
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _violated(assign, deps):
|
| 93 |
+
return [i for i, (d, S) in enumerate(deps)
|
| 94 |
+
if not any(assign[c] == d for c in S)]
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _violations(assign, deps):
|
| 98 |
+
return len(_violated(assign, deps))
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def build_dep_index(deps):
|
| 102 |
+
"""cell -> [(dep index, digit that dep wants), ...]"""
|
| 103 |
+
cell_deps = [[] for _ in range(81)]
|
| 104 |
+
for i, (d, S) in enumerate(deps):
|
| 105 |
+
for c in S:
|
| 106 |
+
cell_deps[c].append((i, d))
|
| 107 |
+
return cell_deps
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def repair(assign, deps, forced, rng, max_iter=200, cell_deps=None):
|
| 111 |
+
"""Min-conflicts repair: repeatedly take a violated disjunction and give its
|
| 112 |
+
digit to whichever of its cells breaks the fewest other disjunctions.
|
| 113 |
+
|
| 114 |
+
Purely a proposal-quality step. The accept/reject test still runs afterwards
|
| 115 |
+
and is the only thing that decides whether an instance enters the dataset.
|
| 116 |
+
Violation counts are maintained incrementally, so each move costs only the
|
| 117 |
+
handful of disjunctions that touch the cell being changed.
|
| 118 |
+
"""
|
| 119 |
+
if not deps:
|
| 120 |
+
return True
|
| 121 |
+
if cell_deps is None:
|
| 122 |
+
cell_deps = build_dep_index(deps)
|
| 123 |
+
counts = [sum(1 for c in S if assign[c] == d) for d, S in deps]
|
| 124 |
+
nviol = sum(1 for x in counts if x == 0)
|
| 125 |
+
|
| 126 |
+
def apply(c, new):
|
| 127 |
+
nonlocal nviol
|
| 128 |
+
old = assign[c]
|
| 129 |
+
if old == new:
|
| 130 |
+
return
|
| 131 |
+
for i, d in cell_deps[c]:
|
| 132 |
+
if d == old:
|
| 133 |
+
counts[i] -= 1
|
| 134 |
+
if counts[i] == 0:
|
| 135 |
+
nviol += 1
|
| 136 |
+
elif d == new:
|
| 137 |
+
if counts[i] == 0:
|
| 138 |
+
nviol -= 1
|
| 139 |
+
counts[i] += 1
|
| 140 |
+
assign[c] = new
|
| 141 |
+
|
| 142 |
+
for _ in range(max_iter):
|
| 143 |
+
if nviol == 0:
|
| 144 |
+
return True
|
| 145 |
+
bad = [i for i, x in enumerate(counts) if x == 0]
|
| 146 |
+
d, S = deps[bad[rng.integers(len(bad))]]
|
| 147 |
+
free = [c for c in S if c not in forced]
|
| 148 |
+
if not free:
|
| 149 |
+
return False
|
| 150 |
+
best, best_v, prev = None, None, {}
|
| 151 |
+
for c in free:
|
| 152 |
+
prev[c] = assign[c]
|
| 153 |
+
before = nviol
|
| 154 |
+
apply(c, d)
|
| 155 |
+
v = nviol
|
| 156 |
+
apply(c, prev[c])
|
| 157 |
+
assert nviol == before
|
| 158 |
+
if best_v is None or v < best_v:
|
| 159 |
+
best, best_v = c, v
|
| 160 |
+
apply(best, d)
|
| 161 |
+
return nviol == 0
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def propose(mask, forced, empties, deps, uncovered, rng, aware=True):
|
| 165 |
+
"""One candidate instance. `aware` first satisfies the confined
|
| 166 |
+
disjunctions, then fills the rest preferring not-yet-covered pairs.
|
| 167 |
+
Without it, every cell is an independent draw from its candidate set."""
|
| 168 |
+
assign = np.zeros(81, dtype=np.int8)
|
| 169 |
+
for c, d in forced.items():
|
| 170 |
+
assign[c] = d
|
| 171 |
+
taken = set(forced)
|
| 172 |
+
|
| 173 |
+
if aware and deps:
|
| 174 |
+
order = sorted(range(len(deps)), key=lambda i: len(deps[i][1]))
|
| 175 |
+
for i in order:
|
| 176 |
+
d, S = deps[i]
|
| 177 |
+
if any(assign[c] == d for c in S):
|
| 178 |
+
continue
|
| 179 |
+
free = [c for c in S if c not in taken]
|
| 180 |
+
if not free:
|
| 181 |
+
continue # unsatisfiable in this proposal; test catches it
|
| 182 |
+
c = free[rng.integers(len(free))]
|
| 183 |
+
assign[c] = d
|
| 184 |
+
taken.add(c)
|
| 185 |
+
|
| 186 |
+
for c in empties:
|
| 187 |
+
if c in taken:
|
| 188 |
+
continue
|
| 189 |
+
cands = digits_of(mask[c])
|
| 190 |
+
unc = [d for d in cands if (c, d) in uncovered]
|
| 191 |
+
pool = unc if unc else cands
|
| 192 |
+
assign[c] = pool[rng.integers(len(pool))]
|
| 193 |
+
return assign
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def instances_for_stage(mask, max_confine=3, max_instances=64,
|
| 197 |
+
max_attempts=400, seed=0, aware=True, max_repair=200,
|
| 198 |
+
require_new=True, patience=60):
|
| 199 |
+
"""Run the generate/test/loop for one stage.
|
| 200 |
+
|
| 201 |
+
Returns a dict with the instances and the statistics the caller wants:
|
| 202 |
+
how many were produced, how many were ruled out, and how much of each
|
| 203 |
+
candidate set the survivors cover.
|
| 204 |
+
"""
|
| 205 |
+
rng = np.random.default_rng(seed)
|
| 206 |
+
forced, empties = split_cells(mask)
|
| 207 |
+
deps = dependencies_from_mask(mask, max_confine)
|
| 208 |
+
|
| 209 |
+
all_pairs = {(c, d) for c in empties for d in digits_of(mask[c])}
|
| 210 |
+
uncovered = set(all_pairs)
|
| 211 |
+
|
| 212 |
+
cell_deps = build_dep_index(deps)
|
| 213 |
+
accepted, rejected, attempts = [], 0, 0
|
| 214 |
+
seen = set()
|
| 215 |
+
if not empties:
|
| 216 |
+
# Fully determined stage (the last one): the single instance is the
|
| 217 |
+
# solution itself, which the loop below would never emit.
|
| 218 |
+
only = np.zeros(81, dtype=np.int8)
|
| 219 |
+
for c, d in forced.items():
|
| 220 |
+
only[c] = d
|
| 221 |
+
accepted.append(only)
|
| 222 |
+
since_new = 0
|
| 223 |
+
while (uncovered and attempts < max_attempts
|
| 224 |
+
and len(accepted) < max_instances and since_new < patience):
|
| 225 |
+
attempts += 1
|
| 226 |
+
inst = propose(mask, forced, empties, deps, uncovered, rng, aware)
|
| 227 |
+
if max_repair:
|
| 228 |
+
repair(inst, deps, forced, rng, max_repair, cell_deps)
|
| 229 |
+
key = inst.tobytes()
|
| 230 |
+
if _violations(inst, deps) != 0 or key in seen:
|
| 231 |
+
rejected += 1
|
| 232 |
+
since_new += 1
|
| 233 |
+
continue
|
| 234 |
+
seen.add(key)
|
| 235 |
+
gained = [(c, int(inst[c])) for c in empties
|
| 236 |
+
if (c, int(inst[c])) in uncovered]
|
| 237 |
+
if require_new and not gained:
|
| 238 |
+
# Valid but redundant: adds no candidate the set already lacks.
|
| 239 |
+
since_new += 1
|
| 240 |
+
continue
|
| 241 |
+
since_new = 0
|
| 242 |
+
accepted.append(inst)
|
| 243 |
+
for pair in gained:
|
| 244 |
+
uncovered.discard(pair)
|
| 245 |
+
|
| 246 |
+
covered = len(all_pairs) - len(uncovered)
|
| 247 |
+
widths = [len(digits_of(mask[c])) for c in empties]
|
| 248 |
+
# A disjunction says "digit d appears in S", never "at most once", so an
|
| 249 |
+
# instance may place the same digit twice in a unit. Count how often.
|
| 250 |
+
dups = []
|
| 251 |
+
for a in accepted:
|
| 252 |
+
n = 0
|
| 253 |
+
for unit in UNITS:
|
| 254 |
+
seen_d = {}
|
| 255 |
+
for c in unit:
|
| 256 |
+
seen_d[a[c]] = seen_d.get(a[c], 0) + 1
|
| 257 |
+
n += sum(v - 1 for v in seen_d.values() if v > 1)
|
| 258 |
+
dups.append(n)
|
| 259 |
+
return {
|
| 260 |
+
"instances": np.array(accepted, dtype=np.int8) if accepted
|
| 261 |
+
else np.zeros((0, 81), dtype=np.int8),
|
| 262 |
+
"n_instances": len(accepted),
|
| 263 |
+
"n_rejected": rejected,
|
| 264 |
+
"n_attempts": attempts,
|
| 265 |
+
"n_deps": len(deps),
|
| 266 |
+
"dep_sizes": [len(S) for _, S in deps],
|
| 267 |
+
"n_empty": len(empties),
|
| 268 |
+
"mean_width": float(np.mean(widths)) if widths else 0.0,
|
| 269 |
+
"n_pairs": len(all_pairs),
|
| 270 |
+
"n_covered": covered,
|
| 271 |
+
"coverage": covered / len(all_pairs) if all_pairs else 1.0,
|
| 272 |
+
"hit_cap": bool(uncovered),
|
| 273 |
+
"mean_dups": float(np.mean(dups)) if dups else 0.0,
|
| 274 |
+
}
|
code/train/data.py
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 143 |
+
def _load_instances(self):
|
| 144 |
+
"""Load superposition instances: one concrete assignment per row.
|
| 145 |
+
|
| 146 |
+
Replaces the multi-hot candidate target with ordinary value tokens. For
|
| 147 |
+
a puzzle at stage s there are several assignments, each picking one digit
|
| 148 |
+
per cell from that cell's stage-s candidate set, so the candidate set is
|
| 149 |
+
recoverable across instances instead of being supervised as a set.
|
| 150 |
+
|
| 151 |
+
assignments: (M, 81) uint8, cell = r*9+c
|
| 152 |
+
starts/counts: (N, S) row range for each (puzzle, stage)
|
| 153 |
+
"""
|
| 154 |
+
self.instances = None
|
| 155 |
+
d = getattr(self.config, "instance_dir", None)
|
| 156 |
+
if not d:
|
| 157 |
+
return
|
| 158 |
+
split = "train" if self.train else "test"
|
| 159 |
+
self.instances = np.load(
|
| 160 |
+
os.path.join(d, f"{split}_assignments.npy"), mmap_mode="r")
|
| 161 |
+
self.inst_starts = np.load(os.path.join(d, f"{split}_starts.npy"))
|
| 162 |
+
self.inst_counts = np.load(os.path.join(d, f"{split}_counts.npy"))
|
| 163 |
+
self.inst_stages = int(self.inst_starts.shape[1])
|
| 164 |
+
print(f"[inst] loaded {split} instances {self.instances.shape} "
|
| 165 |
+
f"over {self.inst_starts.shape[0]} puzzles, "
|
| 166 |
+
f"{self.inst_stages} stages", flush=True)
|
| 167 |
+
|
| 168 |
+
def instance_stage(self):
|
| 169 |
+
"""Stage whose instances this example should target.
|
| 170 |
+
|
| 171 |
+
Bound to the curriculum stage so the target's ambiguity matches the
|
| 172 |
+
latent depth: stage t runs t latent slots and supervises the stage-(t-1)
|
| 173 |
+
assignments, ending at the unique solution when t == S.
|
| 174 |
+
"""
|
| 175 |
+
S = self.inst_stages
|
| 176 |
+
if self.curriculum is None:
|
| 177 |
+
return S - 1
|
| 178 |
+
t = int(np.clip(self.curriculum.stage, 1, S))
|
| 179 |
+
return t - 1
|
| 180 |
+
|
| 181 |
+
def instance_values(self, idx, stage):
|
| 182 |
+
"""One assignment for (puzzle idx, stage), sampled uniformly."""
|
| 183 |
+
n = int(self.inst_counts[idx, stage])
|
| 184 |
+
if n <= 0:
|
| 185 |
+
return None
|
| 186 |
+
row = int(self.inst_starts[idx, stage]) + self.rng.randint(n)
|
| 187 |
+
return np.asarray(self.instances[row])
|
| 188 |
+
|
| 189 |
+
def apply_instance(self, seq, idx, stage):
|
| 190 |
+
"""Rewrite the value token of every triple to this instance's digit.
|
| 191 |
+
|
| 192 |
+
The (row, col) order is untouched, so the clue block and the solver-order
|
| 193 |
+
output sequence are exactly as before; only the values change.
|
| 194 |
+
"""
|
| 195 |
+
vals = self.instance_values(idx, stage)
|
| 196 |
+
if vals is None:
|
| 197 |
+
return seq
|
| 198 |
+
seq = seq.copy()
|
| 199 |
+
cells = seq[0::3].astype(np.int64) * 9 + seq[1::3].astype(np.int64)
|
| 200 |
+
seq[2::3] = vals[cells].astype(seq.dtype)
|
| 201 |
+
return seq
|
| 202 |
+
|
| 203 |
+
def _load_round_bins(self):
|
| 204 |
+
"""Load per-puzzle solver round counts and bin them into max_stage
|
| 205 |
+
equal-count bins, for the round-count DATA curriculum.
|
| 206 |
+
|
| 207 |
+
The round count (waves needed to reach the unique solution, 5..38) is
|
| 208 |
+
the same propagation-depth axis the latent curriculum supervises, but
|
| 209 |
+
used to order the *puzzles* instead of the supervision. Bin edges are
|
| 210 |
+
always computed on the train split and reused for eval so a bin index
|
| 211 |
+
means the same thing in both.
|
| 212 |
+
"""
|
| 213 |
+
self.round_bins = None
|
| 214 |
+
self.num_bins = int(getattr(self.config, "curriculum_max_stage", 12))
|
| 215 |
+
if str(getattr(self.config, "data_curriculum", "none")) != "rounds":
|
| 216 |
+
return
|
| 217 |
+
tr_path = getattr(self.config, "train_meta_path", None)
|
| 218 |
+
path = tr_path if self.train else getattr(
|
| 219 |
+
self.config, "test_meta_path", None)
|
| 220 |
+
if not (path and tr_path):
|
| 221 |
+
raise ValueError(
|
| 222 |
+
"data_curriculum='rounds' needs SUDOKU_TRAIN_META and "
|
| 223 |
+
"SUDOKU_TEST_META (the *_meta.npy written by "
|
| 224 |
+
"staged_candidate_gen.py; column 2 is num_rounds)")
|
| 225 |
+
rounds = np.load(path, mmap_mode="r")[:, 2].astype(np.int32)
|
| 226 |
+
# Cut points from the TRAIN split, so a bin index means the same thing
|
| 227 |
+
# in eval. Round counts are integers with a peaked distribution (mean
|
| 228 |
+
# 22, sd 4), so raw quantiles collide -- the 12-bin quantiles repeat 20
|
| 229 |
+
# twice on the full corpus, which would leave a bin permanently empty
|
| 230 |
+
# and strand its stage with no frontier to measure. Force the cuts
|
| 231 |
+
# strictly increasing so every bin is reachable.
|
| 232 |
+
train_rounds = np.load(tr_path, mmap_mode="r")[:, 2].astype(np.int32)
|
| 233 |
+
edges = np.quantile(train_rounds, np.linspace(0, 1, self.num_bins + 1))
|
| 234 |
+
cuts = np.round(edges[1:-1]).astype(np.int64)
|
| 235 |
+
for i in range(1, len(cuts)):
|
| 236 |
+
if cuts[i] <= cuts[i - 1]:
|
| 237 |
+
cuts[i] = cuts[i - 1] + 1
|
| 238 |
+
# bin j (1-based) = stage that first unlocks the puzzle.
|
| 239 |
+
self.round_bins = np.clip(
|
| 240 |
+
np.searchsorted(cuts, rounds, side="right") + 1,
|
| 241 |
+
1, self.num_bins).astype(np.int32)
|
| 242 |
+
self.bin_index = {b: np.where(self.round_bins == b)[0]
|
| 243 |
+
for b in range(1, self.num_bins + 1)}
|
| 244 |
+
counts = {b: int(len(v)) for b, v in self.bin_index.items()}
|
| 245 |
+
print(f"[rounds] {'train' if self.train else 'eval'} bin counts:",
|
| 246 |
+
counts, flush=True)
|
| 247 |
+
print(f"[rounds] cuts: {cuts.tolist()} (rounds "
|
| 248 |
+
f"{int(rounds.min())}..{int(rounds.max())})", flush=True)
|
| 249 |
+
empty = [b for b, c in counts.items() if c == 0]
|
| 250 |
+
if empty and self.train:
|
| 251 |
+
raise ValueError(
|
| 252 |
+
f"round-bin curriculum has empty train bins {empty}; those "
|
| 253 |
+
f"stages would have no puzzles and no frontier signal")
|
| 254 |
+
|
| 255 |
+
def _load_candidate_masks(self):
|
| 256 |
+
"""Load staged candidate-set masks (N, S, 81) uint16, aligned by puzzle
|
| 257 |
+
index with the loaded .npy. Row i here == puzzle i in the base file."""
|
| 258 |
+
if self.train:
|
| 259 |
+
path = getattr(self.config, "train_cand_masks_path", None)
|
| 260 |
+
else:
|
| 261 |
+
path = getattr(self.config, "test_cand_masks_path", None)
|
| 262 |
+
self.cand_masks = None
|
| 263 |
+
self.num_stages = 0
|
| 264 |
+
if path:
|
| 265 |
+
self.cand_masks = np.load(path, mmap_mode="r")
|
| 266 |
+
self.num_stages = int(self.cand_masks.shape[1])
|
| 267 |
+
print(f"[cand] loaded {path} shape {self.cand_masks.shape}", flush=True)
|
| 268 |
+
|
| 269 |
+
def slot_budget(self, level):
|
| 270 |
+
"""Number of latent slots this example activates (see cand_slot_mode).
|
| 271 |
+
|
| 272 |
+
"depth" mode must agree with the recurrence depth used by the train
|
| 273 |
+
step, since build_latent_state only ever writes slots [0, num_passes):
|
| 274 |
+
supervising a slot the recurrence never filled would train the head off
|
| 275 |
+
an all-zero latent.
|
| 276 |
+
"""
|
| 277 |
+
K = self.num_latent_slots
|
| 278 |
+
if getattr(self.config, "cand_slot_mode", "level") == "depth":
|
| 279 |
+
stage = self.curriculum.stage if self.curriculum is not None \
|
| 280 |
+
else getattr(self.config, "curriculum_max_stage", 6)
|
| 281 |
+
pps = int(getattr(self.config, "passes_per_stage", 1))
|
| 282 |
+
return int(np.clip(pps * stage, 1, K))
|
| 283 |
+
return int(np.clip(level - 2, 1, K))
|
| 284 |
+
|
| 285 |
+
def _slot_stage_targets(self, idx, level, clue_cells=None):
|
| 286 |
+
"""Return (K, 81) int32 candidate bitmasks, one per latent slot.
|
| 287 |
+
|
| 288 |
+
The S stored stages are mapped onto the example's k active slots; see
|
| 289 |
+
cand_slot_mode for the two mappings ("level" re-paces the whole shrink
|
| 290 |
+
sequence into k slots, "depth" assigns slot j to stage j). Inactive slots
|
| 291 |
+
(j>=k) default to the final stage; they are masked out of the loss.
|
| 292 |
+
|
| 293 |
+
Although the array is laid out over all 81 cell positions (for a fixed
|
| 294 |
+
batch shape), the supervised targets are only the *empty* cells: clue
|
| 295 |
+
cells are zeroed out here as a sentinel (a genuine empty cell always has
|
| 296 |
+
>=1 candidate at every stage), and the loss ignores zero rows. So the
|
| 297 |
+
effective target per puzzle is (#empty cells) x 9, in solver order."""
|
| 298 |
+
K = self.num_latent_slots
|
| 299 |
+
if self.cand_masks is None or K == 0:
|
| 300 |
+
return np.zeros((K, 81), dtype=np.int32)
|
| 301 |
+
S = self.num_stages
|
| 302 |
+
k = self.slot_budget(level)
|
| 303 |
+
depth_mode = getattr(self.config, "cand_slot_mode", "level") == "depth"
|
| 304 |
+
stages = self.cand_masks[idx].astype(np.int32) # (S, 81)
|
| 305 |
+
out = np.zeros((K, 81), dtype=np.int32)
|
| 306 |
+
for j in range(K):
|
| 307 |
+
if j >= k:
|
| 308 |
+
# Inactive slot: masked out of the loss, value is irrelevant.
|
| 309 |
+
s = S - 1
|
| 310 |
+
elif depth_mode:
|
| 311 |
+
# Identity: slot j holds propagation block j, so growing the
|
| 312 |
+
# recurrence depth extends the chain instead of re-pacing it.
|
| 313 |
+
# The solution is only reached at full depth, which is what
|
| 314 |
+
# makes this a curriculum over reasoning depth.
|
| 315 |
+
s = min(j, S - 1)
|
| 316 |
+
else:
|
| 317 |
+
# Active slots span the full shrink sequence: slot 0 -> stage 0
|
| 318 |
+
# (widest candidate set, genuinely multi-valued), last active
|
| 319 |
+
# slot -> final stage (solution). For k==1 the single slot maps
|
| 320 |
+
# to the WIDEST set (stage 0), not the solution, so even level-3
|
| 321 |
+
# puzzles give the candidate head a real multi-candidate target
|
| 322 |
+
# (the LM head still produces the unique answer).
|
| 323 |
+
s = int(round(j * (S - 1) / max(k - 1, 1)))
|
| 324 |
+
out[j] = stages[s]
|
| 325 |
+
# Sentinel-zero the clue cells so only the empty cells are supervised.
|
| 326 |
+
if clue_cells is not None and len(clue_cells) > 0:
|
| 327 |
+
out[:, clue_cells] = 0
|
| 328 |
+
return out
|
| 329 |
+
|
| 330 |
+
def _build_level_index(self, levels):
|
| 331 |
+
"""Map difficulty level -> array of puzzle indices."""
|
| 332 |
+
return {lvl: np.where(levels == lvl)[0] for lvl in range(3, 9)}
|
| 333 |
+
|
| 334 |
+
def insert_latent_slots(self, seq, start_index):
|
| 335 |
+
"""Insert K latent placeholder tokens between clues and solution.
|
| 336 |
+
|
| 337 |
+
seq: (243,) triple sequence. Returns (243 + K,) sequence:
|
| 338 |
+
[clues (3*si)] [K placeholders] [solution triples].
|
| 339 |
+
"""
|
| 340 |
+
k = self.num_latent_slots
|
| 341 |
+
if k == 0:
|
| 342 |
+
return seq
|
| 343 |
+
si3 = 3 * int(start_index)
|
| 344 |
+
return np.concatenate([
|
| 345 |
+
seq[:si3],
|
| 346 |
+
np.full(k, self.latent_token_id, dtype=seq.dtype),
|
| 347 |
+
seq[si3:],
|
| 348 |
+
])
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def convert_to_fixed_or_random_order(self, inputs, start_index):
|
| 352 |
+
"""Convert the sequence of moves to either a fixed or random order.
|
| 353 |
+
|
| 354 |
+
Args:
|
| 355 |
+
inputs: a numpy array of shape (num_puzzles, seq_len) containing the
|
| 356 |
+
sequence of moves for each puzzle
|
| 357 |
+
start_index: a numpy array of shape (num_puzzles, 1) containing the starting
|
| 358 |
+
index for each puzzle
|
| 359 |
+
|
| 360 |
+
Returns:
|
| 361 |
+
transformed_input: a numpy array of shape (num_puzzles, seq_len) containing the
|
| 362 |
+
sequence of moves for each puzzle in either a fixed or random order
|
| 363 |
+
"""
|
| 364 |
+
transformed_input = np.zeros_like(inputs)
|
| 365 |
+
|
| 366 |
+
for i in range(len(inputs)):
|
| 367 |
+
cur_seq = inputs[i]
|
| 368 |
+
cur_start_index = start_index[i, 0]
|
| 369 |
+
|
| 370 |
+
# Split the sequence into input and output prompts
|
| 371 |
+
inp_prompt = cur_seq[ :(3 * cur_start_index) ].reshape(-1, 3)
|
| 372 |
+
out_prompt = cur_seq[ (3 * cur_start_index): ].reshape(-1, 3)
|
| 373 |
+
|
| 374 |
+
# Sort the input prompts in a fixed order
|
| 375 |
+
if self.config.seq_order == "fixed":
|
| 376 |
+
transformed_input[i, :(3 * cur_start_index) ] = inp_prompt[ np.lexsort( inp_prompt[:, ::-1].T ) ].flatten()
|
| 377 |
+
# Randomly shuffle the input prompts
|
| 378 |
+
elif self.config.seq_order == "random":
|
| 379 |
+
transformed_input[i, :(3 * cur_start_index) ] = np.random.permutation(inp_prompt).flatten()
|
| 380 |
+
|
| 381 |
+
# Sort the output prompts in a fixed order
|
| 382 |
+
if self.config.seq_order == "fixed":
|
| 383 |
+
transformed_input[i, (3 * cur_start_index): ] = out_prompt[ np.lexsort( out_prompt[:, ::-1].T ) ].flatten()
|
| 384 |
+
# Randomly shuffle the output prompts
|
| 385 |
+
elif self.config.seq_order == "random":
|
| 386 |
+
transformed_input[i, (3 * cur_start_index): ] = np.random.permutation(out_prompt).flatten()
|
| 387 |
+
|
| 388 |
+
return transformed_input
|
| 389 |
+
|
| 390 |
+
def get_puzzles_start_index(self, path):
|
| 391 |
+
"""Get the puzzles, start index, inputs and difficulty levels.
|
| 392 |
+
|
| 393 |
+
Returns:
|
| 394 |
+
inputs: (num_puzzles, 243) move sequences (strategy column removed)
|
| 395 |
+
puzzles: (num_puzzles, 81) solutions
|
| 396 |
+
start_index: (num_puzzles, 1) number of clue cells
|
| 397 |
+
levels: (num_puzzles,) puzzle difficulty level in [3, 8]
|
| 398 |
+
(= hardest solver-strategy digit needed by any cell)
|
| 399 |
+
"""
|
| 400 |
+
with gfile.Open(path, "rb") as f:
|
| 401 |
+
inputs_with_start_index = np.load(f)
|
| 402 |
+
start_index = inputs_with_start_index[:, 0] # Get the start index
|
| 403 |
+
|
| 404 |
+
rest = inputs_with_start_index[:, 1:]
|
| 405 |
+
# Strategy chain codes (4th entry of each cell quadruple); keep them to
|
| 406 |
+
# derive the curriculum difficulty level, then remove from the inputs.
|
| 407 |
+
strategy_codes = rest.reshape(len(rest), 81, 4)[:, :, 3]
|
| 408 |
+
levels = compute_puzzle_levels(strategy_codes, start_index)
|
| 409 |
+
inputs = np.delete( rest, np.arange(81) * 4 + 3, axis=1)
|
| 410 |
+
|
| 411 |
+
puzzles = np.zeros((len(inputs), 81), dtype=np.int8) # Initialize puzzles
|
| 412 |
+
for j in range(81):
|
| 413 |
+
cell_id = inputs[:, 3 * j] * 9 + inputs[:, 3 * j + 1] # Get the cell id
|
| 414 |
+
puzzles[np.arange(len(inputs)), cell_id] = inputs[:, 3 * j + 2] # Set the puzzle
|
| 415 |
+
|
| 416 |
+
return inputs, puzzles, start_index.reshape(-1, 1), levels
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def preprocess_sudoku(self):
|
| 420 |
+
"""Preprocess the sudoku for train and test datasets.
|
| 421 |
+
|
| 422 |
+
Depending on the `train` flag, this method loads and processes the
|
| 423 |
+
sudoku puzzles and their start indices from the appropriate paths, and
|
| 424 |
+
optionally converts them to a fixed or random order based on the
|
| 425 |
+
configuration.
|
| 426 |
+
"""
|
| 427 |
+
if self.train is True:
|
| 428 |
+
# Load train puzzles, inputs, and start indices
|
| 429 |
+
(self.train_inputs, self.train_puzzles, self.train_start_index,
|
| 430 |
+
self.train_levels) = (
|
| 431 |
+
self.get_puzzles_start_index(self.config.train_puzzle_path)
|
| 432 |
+
)
|
| 433 |
+
# Convert train inputs to fixed or random order if specified
|
| 434 |
+
if self.config.seq_order in {"fixed", "random"}:
|
| 435 |
+
self.train_inputs = self.convert_to_fixed_or_random_order(self.train_inputs, self.train_start_index)
|
| 436 |
+
self.level_index = self._build_level_index(self.train_levels)
|
| 437 |
+
print("train level counts:",
|
| 438 |
+
{l: len(v) for l, v in self.level_index.items()}, flush=True)
|
| 439 |
+
|
| 440 |
+
elif self.train is False:
|
| 441 |
+
# Load evaluation puzzles, inputs, and start indices
|
| 442 |
+
(self.eval_inputs, self.eval_puzzles, self.eval_start_index,
|
| 443 |
+
self.eval_levels) = (
|
| 444 |
+
self.get_puzzles_start_index(self.config.test_puzzle_path)
|
| 445 |
+
)
|
| 446 |
+
# Convert evaluation inputs to fixed or random order if specified
|
| 447 |
+
if self.config.seq_order in {"fixed", "random"}:
|
| 448 |
+
self.eval_inputs = self.convert_to_fixed_or_random_order(self.eval_inputs, self.eval_start_index)
|
| 449 |
+
self.level_index = self._build_level_index(self.eval_levels)
|
| 450 |
+
|
| 451 |
+
def __len__(self):
|
| 452 |
+
if self.train is True:
|
| 453 |
+
return len(self.train_puzzles)
|
| 454 |
+
elif self.train is False:
|
| 455 |
+
return len(self.eval_puzzles)
|
| 456 |
+
|
| 457 |
+
def __getitem__(self, idx):
|
| 458 |
+
"""Returns one example: (sequence with latent slots, solution,
|
| 459 |
+
start_index, difficulty level).
|
| 460 |
+
|
| 461 |
+
The base sequence is 243 tokens of (row, column, value) triples; K
|
| 462 |
+
latent placeholder tokens are inserted after the clue block, giving
|
| 463 |
+
243 + K tokens. start_index is the number of clue cells; level in
|
| 464 |
+
[3, 8] is the hardest solver strategy needed by any cell.
|
| 465 |
+
"""
|
| 466 |
+
if self.train is True:
|
| 467 |
+
inputs, puzzles = self.train_inputs, self.train_puzzles
|
| 468 |
+
start_index, levels = self.train_start_index, self.train_levels
|
| 469 |
+
else:
|
| 470 |
+
inputs, puzzles = self.eval_inputs, self.eval_puzzles
|
| 471 |
+
start_index, levels = self.eval_start_index, self.eval_levels
|
| 472 |
+
|
| 473 |
+
base = inputs[idx, :]
|
| 474 |
+
if self.instances is not None and self.train:
|
| 475 |
+
# Same input prompt, different output prompt: the clue triples are
|
| 476 |
+
# untouched (their instance digit is the clue) while the empty cells
|
| 477 |
+
# take one draw from the stage's candidate sets. Averaged over the
|
| 478 |
+
# instances the target IS the candidate set, so the superposition is
|
| 479 |
+
# learned from ordinary next-token CE instead of a set head. Eval
|
| 480 |
+
# keeps the unique solution: the sequence it scores is generated, and
|
| 481 |
+
# `puzzles` must stay the ground truth the accuracy is measured on.
|
| 482 |
+
base = self.apply_instance(base, idx, self.instance_stage())
|
| 483 |
+
seq = self.insert_latent_slots(base, start_index[idx, 0])
|
| 484 |
+
# Clue cells = the first `start_index` (r,c,v) triples; their cell ids
|
| 485 |
+
# are excluded from candidate supervision (only empty cells are scored).
|
| 486 |
+
si = int(start_index[idx, 0])
|
| 487 |
+
clue_triples = inputs[idx, :3 * si].reshape(-1, 3)
|
| 488 |
+
clue_cells = (clue_triples[:, 0] * 9 + clue_triples[:, 1]).astype(np.int64)
|
| 489 |
+
cand_targets = self._slot_stage_targets(idx, int(levels[idx]), clue_cells)
|
| 490 |
+
rbin = (int(self.round_bins[idx]) if self.round_bins is not None else 0)
|
| 491 |
+
return (
|
| 492 |
+
seq,
|
| 493 |
+
puzzles[idx, :],
|
| 494 |
+
start_index[idx],
|
| 495 |
+
np.array([levels[idx]], dtype=np.int32),
|
| 496 |
+
cand_targets,
|
| 497 |
+
np.array([rbin], dtype=np.int32),
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
def _sample_level(self):
|
| 501 |
+
"""Uniform over levels that have puzzles. Used for level-balanced mode
|
| 502 |
+
and for eval, where per-level reporting needs every level represented."""
|
| 503 |
+
avail = [l for l in range(3, 9) if len(self.level_index[l]) > 0]
|
| 504 |
+
return avail[self.rng.randint(len(avail))]
|
| 505 |
+
|
| 506 |
+
def _sample_round_gated(self):
|
| 507 |
+
"""Uniform over puzzles whose round-bin is already unlocked (bin<=stage).
|
| 508 |
+
Reading the stage at yield time lets the pool grow on promotion."""
|
| 509 |
+
stage = self.curriculum.stage if self.curriculum is not None \
|
| 510 |
+
else self.num_bins
|
| 511 |
+
stage = int(np.clip(stage, 1, self.num_bins))
|
| 512 |
+
pool = np.concatenate([self.bin_index[b] for b in range(1, stage + 1)])
|
| 513 |
+
return int(pool[self.rng.randint(len(pool))])
|
| 514 |
+
|
| 515 |
+
def __call__(self):
|
| 516 |
+
# Infinite generator. Train draws puzzles uniformly from the whole
|
| 517 |
+
# corpus (natural difficulty mix, ~68% level 3): the difficulty tag
|
| 518 |
+
# selects nothing, since the curriculum axis is propagation depth.
|
| 519 |
+
# Eval stays level-balanced so per-level accuracy is measurable and
|
| 520 |
+
# comparable across runs.
|
| 521 |
+
#
|
| 522 |
+
# The exception is the round-count DATA curriculum, where the train pool
|
| 523 |
+
# is restricted to puzzles needing at most stage-many propagation waves.
|
| 524 |
+
# Eval is never gated: it must score the whole corpus at every stage.
|
| 525 |
+
round_gated = (self.train and self.round_bins is not None)
|
| 526 |
+
level_balanced = (not self.train) or bool(
|
| 527 |
+
int(getattr(self.config, "level_balanced_sampling", 0)))
|
| 528 |
+
n = len(self.train_puzzles) if self.train else len(self.eval_puzzles)
|
| 529 |
+
while True:
|
| 530 |
+
if round_gated:
|
| 531 |
+
idx = self._sample_round_gated()
|
| 532 |
+
elif level_balanced:
|
| 533 |
+
idx_arr = self.level_index[self._sample_level()]
|
| 534 |
+
idx = int(idx_arr[self.rng.randint(len(idx_arr))])
|
| 535 |
+
else:
|
| 536 |
+
idx = int(self.rng.randint(n))
|
| 537 |
+
yield self.__getitem__(idx)
|
code/train/evaluater.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluation related functions."""
|
| 2 |
+
|
| 3 |
+
from flax.training import common_utils
|
| 4 |
+
import jax
|
| 5 |
+
from jax import numpy as jnp
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
from train import model
|
| 9 |
+
|
| 10 |
+
import pdb
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def valid_solution(output_seq):
|
| 15 |
+
"""
|
| 16 |
+
This function checks if the puzzle is a valid solution by verifying if
|
| 17 |
+
each row, column and box has all the numbers from 1 to 9.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
output_seq: a numpy array of shape (243,) containing the sequence of
|
| 21 |
+
output numbers
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
int: 1 if correct solution, otherwise returns 0
|
| 25 |
+
"""
|
| 26 |
+
# rows[i, j] keeps track if ith row has received (j + 1) number
|
| 27 |
+
rows = np.zeros((9, 9))
|
| 28 |
+
# cols[i, j] keeps track if ith column has received (j + 1) number
|
| 29 |
+
cols = np.zeros((9, 9))
|
| 30 |
+
# boxes[i, j] keeps track if ith box has received (j + 1) number
|
| 31 |
+
boxes = np.zeros((9, 9))
|
| 32 |
+
|
| 33 |
+
for j in range(81):
|
| 34 |
+
# The row and column are in the range (0, 8) and puzzle values are in (1, 9)
|
| 35 |
+
if int(output_seq[3 * j]) >= 9:
|
| 36 |
+
return False
|
| 37 |
+
if int(output_seq[3 * j + 1]) >= 9:
|
| 38 |
+
return False
|
| 39 |
+
if int(output_seq[3 * j + 2]) > 9:
|
| 40 |
+
return False
|
| 41 |
+
|
| 42 |
+
row_num = int(output_seq[3 * j])
|
| 43 |
+
col_num = int(output_seq[3 * j + 1])
|
| 44 |
+
|
| 45 |
+
# Mark the number in the row, column and box
|
| 46 |
+
rows[row_num, int(output_seq[3 * j + 2] - 1)] += 1
|
| 47 |
+
cols[col_num, int(output_seq[3 * j + 2] - 1)] += 1
|
| 48 |
+
boxes[
|
| 49 |
+
int(3 * (row_num // 3) + (col_num // 3)), int(output_seq[3 * j + 2] - 1)
|
| 50 |
+
] += 1
|
| 51 |
+
|
| 52 |
+
if np.all(rows) and np.all(cols) and np.all(boxes):
|
| 53 |
+
return True
|
| 54 |
+
else:
|
| 55 |
+
return False
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def eval_step(state, batch, latent_vals, slot_pos, latent_active, config):
|
| 59 |
+
pred_logits, hidden, cand_logits = model.TransformerLMHeadModel(config).apply(
|
| 60 |
+
{"params": state.params}, batch, latent_values=latent_vals,
|
| 61 |
+
latent_positions=slot_pos, latent_active=latent_active,
|
| 62 |
+
)
|
| 63 |
+
return pred_logits, hidden, cand_logits
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def verify_sudoku_board(puzzle, row_num, col_num, num):
|
| 67 |
+
"""
|
| 68 |
+
Args:
|
| 69 |
+
puzzle (np.array): The correct Sudoku puzzle.
|
| 70 |
+
row_num (int): The row number (0-8).
|
| 71 |
+
col_num (int): The column number (0-8).
|
| 72 |
+
num (int): The number predicted at the specified row and column.
|
| 73 |
+
|
| 74 |
+
Raises:
|
| 75 |
+
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.
|
| 76 |
+
"""
|
| 77 |
+
if row_num * 9 + col_num >= 81:
|
| 78 |
+
assert False
|
| 79 |
+
|
| 80 |
+
assert puzzle[row_num * 9 + col_num] == num
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def get_eval_metrics(state, eval_data_iter, p_eval_step, config):
|
| 84 |
+
"""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.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
state: contains model parameters, optimizer, etc.
|
| 88 |
+
eval_data_iter: data iterator for evaluation dataset
|
| 89 |
+
p_eval_step: pmap function for forward pass of model for evaluation
|
| 90 |
+
config: general experiment config file
|
| 91 |
+
|
| 92 |
+
Returns:
|
| 93 |
+
eval_metrics: contains list of evaluation metrics for each batch
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
eval_metrics = {
|
| 97 |
+
"acc": [], # Value/placement acc: correct digit at the model-chosen cell
|
| 98 |
+
"loc_acc": [], # Location acc: model picks the ground-truth next cell (r,c)
|
| 99 |
+
"val_given_loc_acc": [], # Correct digit AMONG steps where location matched
|
| 100 |
+
"cand_bit_acc": [], # Per-digit accuracy of predicted candidate masks
|
| 101 |
+
"cand_set_acc": [], # Exact candidate-SET match per empty cell (all 9 bits)
|
| 102 |
+
"cand_set_acc_changed": [], # ...restricted to cells that changed this stage
|
| 103 |
+
"acc_complete_puzzle": [] # Accuracy of predicting correct complete puzzle
|
| 104 |
+
}
|
| 105 |
+
# Per-difficulty-level cell accuracy (levels 3..8). Diagnostic only: the
|
| 106 |
+
# curriculum no longer keys on level.
|
| 107 |
+
level_ok = {lvl: 0 for lvl in range(3, 9)}
|
| 108 |
+
level_tot = {lvl: 0 for lvl in range(3, 9)}
|
| 109 |
+
|
| 110 |
+
K = int(config.num_latent_slots)
|
| 111 |
+
|
| 112 |
+
# Per-SLOT candidate-set accuracy, i.e. per reasoning depth. Slot j holds
|
| 113 |
+
# wave snapshot j, so slot_ok[j]/slot_tot[j] is "how well is propagation
|
| 114 |
+
# block j predicted". This is the signal the depth curriculum promotes and
|
| 115 |
+
# backtracks on, replacing the old per-level accuracy.
|
| 116 |
+
slot_ok = np.zeros(max(K, 1), dtype=np.int64)
|
| 117 |
+
slot_tot = np.zeros(max(K, 1), dtype=np.int64)
|
| 118 |
+
slot_ok_ch = np.zeros(max(K, 1), dtype=np.int64)
|
| 119 |
+
slot_tot_ch = np.zeros(max(K, 1), dtype=np.int64)
|
| 120 |
+
|
| 121 |
+
# Per-stage in-set rate: was the emitted digit a *member* of that stage's
|
| 122 |
+
# candidate set? This is the promotion signal for the instance arm, where
|
| 123 |
+
# the target is one sampled assignment rather than the unique solution, so
|
| 124 |
+
# the model is right to emit any candidate. The candidate masks are read
|
| 125 |
+
# here as a metric only; nothing supervises them.
|
| 126 |
+
inset_ok = np.zeros(max(K, 1), dtype=np.int64)
|
| 127 |
+
inset_tot = np.zeros(max(K, 1), dtype=np.int64)
|
| 128 |
+
|
| 129 |
+
# Per-round-bin cell accuracy, for the round-count DATA curriculum: bin b is
|
| 130 |
+
# unlocked at stage b, so bin_ok[b]/bin_tot[b] measures competence on the
|
| 131 |
+
# puzzles that stage b introduced. This is the promotion signal for the arm
|
| 132 |
+
# that has no latent slots and therefore no per-depth signal.
|
| 133 |
+
n_bins = int(getattr(config, "curriculum_max_stage", 12))
|
| 134 |
+
bin_ok = {b: 0 for b in range(1, n_bins + 1)}
|
| 135 |
+
bin_tot = {b: 0 for b in range(1, n_bins + 1)}
|
| 136 |
+
|
| 137 |
+
for eval_epoch in range(config.eval_epochs):
|
| 138 |
+
with jax.profiler.StepTraceAnnotation("eval", step_num=eval_epoch):
|
| 139 |
+
|
| 140 |
+
batch_tuple = next(eval_data_iter)
|
| 141 |
+
|
| 142 |
+
# Input seq is (batchsize, 3*81 + K): clue triples, K latent
|
| 143 |
+
# placeholder slots, then solution triples.
|
| 144 |
+
input_seq = np.array(batch_tuple[0])
|
| 145 |
+
|
| 146 |
+
# Puzzle solution is of the shape (batchsize, 81). Each pos in {0,.., 80}
|
| 147 |
+
# for each puzzle contains value at cell (pos//9+1, pos%9 + 1)
|
| 148 |
+
puzzle_sol = np.array(batch_tuple[1])
|
| 149 |
+
start_index = np.array(batch_tuple[2])
|
| 150 |
+
levels = np.array(batch_tuple[3]).reshape(-1)
|
| 151 |
+
rbins = (np.array(batch_tuple[5]).reshape(-1)
|
| 152 |
+
if len(batch_tuple) > 5 else np.zeros_like(levels))
|
| 153 |
+
total_pred, sucess_pred = 0, 0
|
| 154 |
+
# Location = did the model emit the ground-truth next (r,c) cell.
|
| 155 |
+
loc_tot, loc_ok, val_given_loc_ok = 0, 0, 0
|
| 156 |
+
|
| 157 |
+
bs = input_seq.shape[0]
|
| 158 |
+
bidx = np.arange(bs)
|
| 159 |
+
si3 = 3 * start_index.reshape(-1)
|
| 160 |
+
slot_pos = si3[:, None] + np.arange(K)[None, :]
|
| 161 |
+
if getattr(config, "cand_slot_mode", "level") == "depth":
|
| 162 |
+
# Eval always builds all K latents, so score all K slots.
|
| 163 |
+
k_budget = np.full_like(levels, K)
|
| 164 |
+
else:
|
| 165 |
+
k_budget = np.clip(levels - 2, 1, K)
|
| 166 |
+
active_full = np.arange(K)[None, :] < k_budget[:, None]
|
| 167 |
+
|
| 168 |
+
def run_model(seq_batch, latent_vals, act, want_cand=False):
|
| 169 |
+
sharded = common_utils.shard(
|
| 170 |
+
jax.tree_util.tree_map(np.asarray, seq_batch))
|
| 171 |
+
# Explicit reshape so a zero-width slot dim (K=0 baseline)
|
| 172 |
+
# shards without the ambiguous -1 inference of shard().
|
| 173 |
+
_nd = jax.local_device_count()
|
| 174 |
+
def _shard(x):
|
| 175 |
+
x = np.asarray(x)
|
| 176 |
+
return x.reshape((_nd, x.shape[0] // _nd) + x.shape[1:])
|
| 177 |
+
lv = _shard(latent_vals)
|
| 178 |
+
lp = _shard(slot_pos)
|
| 179 |
+
la = _shard(act)
|
| 180 |
+
logits, hidden, cand = p_eval_step(state, sharded, lv, lp, la)
|
| 181 |
+
logits = np.array(logits).reshape(bs, *np.array(logits).shape[2:])
|
| 182 |
+
hidden = np.array(hidden).reshape(bs, *np.array(hidden).shape[2:])
|
| 183 |
+
if want_cand:
|
| 184 |
+
cand = np.array(cand).reshape(bs, *np.array(cand).shape[2:])
|
| 185 |
+
return logits, hidden, cand
|
| 186 |
+
return logits, hidden
|
| 187 |
+
|
| 188 |
+
# ---- Build the continuous latent thoughts (K recurrence passes,
|
| 189 |
+
# difficulty-matched budget; causal masking means only the clue
|
| 190 |
+
# region influences them). ----
|
| 191 |
+
latent_vals = np.zeros((bs, K, config.emb_dim), dtype=np.float32)
|
| 192 |
+
build_seq = np.array(input_seq)
|
| 193 |
+
build_seq_masked = np.array(build_seq)
|
| 194 |
+
# Hide the solution region during latent build (safety; causality
|
| 195 |
+
# already prevents leakage into slot hiddens).
|
| 196 |
+
for j in range(bs):
|
| 197 |
+
build_seq_masked[j, si3[j] + K:] = 0
|
| 198 |
+
# Recurrent feedback: build each latent thought from the previous
|
| 199 |
+
# slot's hidden. Skipped when the model does not inject latents
|
| 200 |
+
# (no-recurrence control): slots stay as static placeholders, so
|
| 201 |
+
# latent_vals is left at zeros and never used.
|
| 202 |
+
recurrent = bool(int(getattr(config, "recurrent_latent", 1)))
|
| 203 |
+
if recurrent and K > 0:
|
| 204 |
+
for j in range(K):
|
| 205 |
+
act_j = active_full & (np.arange(K)[None, :] < j)
|
| 206 |
+
_, hidden = run_model(build_seq_masked, latent_vals, act_j)
|
| 207 |
+
src = si3 - 1 + j
|
| 208 |
+
latent_vals[:, j] = hidden[bidx, src]
|
| 209 |
+
|
| 210 |
+
# ---- Candidate-set prediction accuracy (the multi-value target) ----
|
| 211 |
+
# One forward pass with the fully-built latents; read the per-slot
|
| 212 |
+
# candidate head and compare to the staged bitmask targets, scored
|
| 213 |
+
# only over active slots and empty cells (clue cells were zeroed).
|
| 214 |
+
# Skipped entirely for the K=0 no-latent baseline (no candidate head).
|
| 215 |
+
pred_bits = tgt_bits = cand_targets = None
|
| 216 |
+
if K > 0:
|
| 217 |
+
cand_targets = np.array(batch_tuple[4]).astype(np.int64) # (bs, K, 81)
|
| 218 |
+
# The candidate head is off in the instance arm (aux weight 0), so
|
| 219 |
+
# skip its forward pass and set metrics; the masks above are still
|
| 220 |
+
# read for the in-set rate.
|
| 221 |
+
if K > 0 and float(getattr(config, "aux_cand_weight", 1.0)) > 0.0:
|
| 222 |
+
_, _, cand_logits = run_model(
|
| 223 |
+
build_seq_masked, latent_vals, active_full, want_cand=True) # (bs,K,81,9)
|
| 224 |
+
pred_bits = (np.array(cand_logits) > 0.0) # sigmoid>0.5
|
| 225 |
+
tgt_bits = ((cand_targets[..., None] >> np.arange(9)) & 1).astype(bool)
|
| 226 |
+
valid = (cand_targets > 0) & active_full[:, :, None] # (bs,K,81)
|
| 227 |
+
if valid.sum() > 0:
|
| 228 |
+
bit_match = (pred_bits == tgt_bits) # (bs,K,81,9)
|
| 229 |
+
eval_metrics["cand_bit_acc"].append(
|
| 230 |
+
float(bit_match[valid].mean()))
|
| 231 |
+
eval_metrics["cand_set_acc"].append(
|
| 232 |
+
float(bit_match.all(axis=3)[valid].mean()))
|
| 233 |
+
# Same score restricted to cells whose candidate set
|
| 234 |
+
# actually changed from the previous stage. The unrestricted
|
| 235 |
+
# metrics above are dominated by cells that are unchanged
|
| 236 |
+
# copies of slot j-1, so they stay high for a head that has
|
| 237 |
+
# learned nothing but "repeat the previous slot".
|
| 238 |
+
changed = np.concatenate(
|
| 239 |
+
[np.ones_like(cand_targets[:, :1], dtype=bool),
|
| 240 |
+
cand_targets[:, 1:] != cand_targets[:, :-1]], axis=1)
|
| 241 |
+
valid_ch = valid & changed
|
| 242 |
+
if valid_ch.sum() > 0:
|
| 243 |
+
eval_metrics["cand_set_acc_changed"].append(
|
| 244 |
+
float(bit_match.all(axis=3)[valid_ch].mean()))
|
| 245 |
+
# Accumulate the same score split by slot (= depth).
|
| 246 |
+
set_match = bit_match.all(axis=3) # (bs,K,81)
|
| 247 |
+
slot_ok += (set_match & valid).sum(axis=(0, 2))
|
| 248 |
+
slot_tot += valid.sum(axis=(0, 2))
|
| 249 |
+
slot_ok_ch += (set_match & valid_ch).sum(axis=(0, 2))
|
| 250 |
+
slot_tot_ch += valid_ch.sum(axis=(0, 2))
|
| 251 |
+
|
| 252 |
+
min_start_index = int(np.min(start_index))
|
| 253 |
+
cur_input_seq = input_seq[:, :(min_start_index*3)]
|
| 254 |
+
for i in range(min_start_index * 3, config.seq_len):
|
| 255 |
+
### In i^th iteration, i^th number in sequence will predict
|
| 256 |
+
padding = np.zeros((input_seq.shape[0],
|
| 257 |
+
config.seq_len - len(cur_input_seq[0])),
|
| 258 |
+
dtype=np.int32)
|
| 259 |
+
concat_batch = np.hstack((cur_input_seq, padding))
|
| 260 |
+
|
| 261 |
+
pred_logits, _ = run_model(concat_batch, latent_vals, active_full)
|
| 262 |
+
|
| 263 |
+
# Positions < 3*start_index + K are given (clues + latent
|
| 264 |
+
# slots); the model predicts from there on. K is a multiple
|
| 265 |
+
# of 3, so the triple phase of i is unchanged.
|
| 266 |
+
if i%3 == 2:
|
| 267 |
+
# Model predicts the value at the cell (cur_input_seq[j][i-2],
|
| 268 |
+
# cur_input_seq[j][i-1])
|
| 269 |
+
max_number = pred_logits[:, i-1, :].argmax(axis=-1).flatten()
|
| 270 |
+
mask_arr = np.array(i >= (3 * start_index + K)).squeeze()
|
| 271 |
+
|
| 272 |
+
next_number = max_number * mask_arr + (1 - mask_arr) * input_seq[:, i]
|
| 273 |
+
|
| 274 |
+
cur_input_seq = np.hstack(
|
| 275 |
+
(cur_input_seq, np.reshape(next_number, (-1, 1)))
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
# Iterate through all examples in batch and calculate successful
|
| 279 |
+
# predictions of numbers
|
| 280 |
+
for j in range(len(cur_input_seq)):
|
| 281 |
+
if not mask_arr[j]:
|
| 282 |
+
continue
|
| 283 |
+
|
| 284 |
+
total_pred += 1
|
| 285 |
+
level_tot[int(levels[j])] += 1
|
| 286 |
+
if int(rbins[j]) in bin_tot:
|
| 287 |
+
bin_tot[int(rbins[j])] += 1
|
| 288 |
+
|
| 289 |
+
# Location accuracy: did the model emit the ground-truth
|
| 290 |
+
# next cell (r,c) for this solver-order step?
|
| 291 |
+
loc_tot += 1
|
| 292 |
+
loc_match = (int(cur_input_seq[j][i-2]) == int(input_seq[j, i-2])
|
| 293 |
+
and int(cur_input_seq[j][i-1]) == int(input_seq[j, i-1]))
|
| 294 |
+
if loc_match:
|
| 295 |
+
loc_ok += 1
|
| 296 |
+
|
| 297 |
+
# In-set rate per stage, scored at the ground-truth cell
|
| 298 |
+
# so a wrong location cannot make a digit vacuously
|
| 299 |
+
# legal. cand_targets[j, s, cell] is stage s's bitmask
|
| 300 |
+
# under cand_slot_mode="depth" (slot s <-> stage s).
|
| 301 |
+
if cand_targets is not None and loc_match:
|
| 302 |
+
cell = (int(input_seq[j, i-2]) * 9
|
| 303 |
+
+ int(input_seq[j, i-1]))
|
| 304 |
+
v = int(cur_input_seq[j][i])
|
| 305 |
+
for s in range(K):
|
| 306 |
+
bits = int(cand_targets[j, s, cell])
|
| 307 |
+
if bits <= 0: # clue cell, not supervised
|
| 308 |
+
continue
|
| 309 |
+
inset_tot[s] += 1
|
| 310 |
+
if 1 <= v <= 9 and (bits >> (v - 1)) & 1:
|
| 311 |
+
inset_ok[s] += 1
|
| 312 |
+
|
| 313 |
+
try:
|
| 314 |
+
verify_sudoku_board(puzzle_sol[j], cur_input_seq[j][i-2],
|
| 315 |
+
cur_input_seq[j][i-1], cur_input_seq[j][i])
|
| 316 |
+
except AssertionError:
|
| 317 |
+
# Mistake
|
| 318 |
+
pass
|
| 319 |
+
else:
|
| 320 |
+
sucess_pred += 1
|
| 321 |
+
level_ok[int(levels[j])] += 1
|
| 322 |
+
if int(rbins[j]) in bin_ok:
|
| 323 |
+
bin_ok[int(rbins[j])] += 1
|
| 324 |
+
if loc_match:
|
| 325 |
+
val_given_loc_ok += 1
|
| 326 |
+
else:
|
| 327 |
+
# Model predicts either a row number or column number
|
| 328 |
+
max_pos = pred_logits[:, i-1, :].argmax(axis=-1).flatten()
|
| 329 |
+
mask = (i >= (3 * start_index + K)).squeeze()
|
| 330 |
+
next_pos = max_pos * mask + (1 - mask) * input_seq[:, i]
|
| 331 |
+
|
| 332 |
+
# pdb.set_trace()
|
| 333 |
+
cur_input_seq = np.hstack(
|
| 334 |
+
(cur_input_seq, np.reshape(next_pos, (-1, 1)))
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
eval_metrics["acc"].append(sucess_pred * 1.0/ total_pred)
|
| 338 |
+
eval_metrics["loc_acc"].append(loc_ok * 1.0 / max(loc_tot, 1))
|
| 339 |
+
eval_metrics["val_given_loc_acc"].append(
|
| 340 |
+
val_given_loc_ok * 1.0 / max(loc_ok, 1))
|
| 341 |
+
|
| 342 |
+
def strip_latent_slots(seq, si):
|
| 343 |
+
return np.concatenate([seq[:3*si], seq[3*si + K:]])
|
| 344 |
+
|
| 345 |
+
# ---- Print one concrete example answer the model generated ----
|
| 346 |
+
if eval_epoch == 0:
|
| 347 |
+
j = 0
|
| 348 |
+
si = int(start_index[j, 0])
|
| 349 |
+
pred = strip_latent_slots(cur_input_seq[j], si)
|
| 350 |
+
shown, n_ok, n_tot = [], 0, 0
|
| 351 |
+
for k in range(si, 81):
|
| 352 |
+
r, c, v = int(pred[3*k]), int(pred[3*k+1]), int(pred[3*k+2])
|
| 353 |
+
tv = int(puzzle_sol[j][r*9+c]) if (0 <= r < 9 and 0 <= c < 9) else -1
|
| 354 |
+
ok = (0 <= r < 9 and 0 <= c < 9 and v == tv)
|
| 355 |
+
n_tot += 1; n_ok += int(ok)
|
| 356 |
+
if len(shown) < 12:
|
| 357 |
+
shown.append(f"({r},{c})->{v}[true {tv}]{'ok' if ok else 'X'}")
|
| 358 |
+
print(f"EXAMPLE (level={int(levels[j])}, k={int(k_budget[j])}): "
|
| 359 |
+
f"model emitted {n_tot} (r,c)->v triples for the empty cells "
|
| 360 |
+
f"(format: (row,col)->value[true T]); first 12:", flush=True)
|
| 361 |
+
print(" ", " ".join(shown), flush=True)
|
| 362 |
+
print(f"EXAMPLE cells-correct={n_ok}/{n_tot} "
|
| 363 |
+
f"valid_full_grid={valid_solution(pred)}", flush=True)
|
| 364 |
+
|
| 365 |
+
# Instance arm: emitted digit next to the deepest stage's
|
| 366 |
+
# candidate set, so it is visible whether the model is sitting
|
| 367 |
+
# inside the superposition or outside it.
|
| 368 |
+
if K > 0 and cand_targets is not None and pred_bits is None:
|
| 369 |
+
tgt = strip_latent_slots(input_seq[j], si)
|
| 370 |
+
shown = []
|
| 371 |
+
for t3 in range(si, min(si + 8, 81)):
|
| 372 |
+
r, c = int(tgt[3*t3]), int(tgt[3*t3+1])
|
| 373 |
+
bits = int(cand_targets[j, K-1, r*9+c])
|
| 374 |
+
cset = "".join(str(d+1) for d in range(9)
|
| 375 |
+
if (bits >> d) & 1)
|
| 376 |
+
shown.append(f"(r{r},c{c})->{int(pred[3*t3+2])} "
|
| 377 |
+
f"in{{{cset}}}")
|
| 378 |
+
print(f"EXAMPLE emitted vs stage-{K} candidate set:",
|
| 379 |
+
" ".join(shown), flush=True)
|
| 380 |
+
|
| 381 |
+
# ---- Candidate-set (multi-value) prediction for this puzzle ----
|
| 382 |
+
# Show, at the last active latent slot, predicted vs target
|
| 383 |
+
# candidate SETS for the first few empty cells. (No latent
|
| 384 |
+
# slots in the K=0 baseline, so nothing to show.)
|
| 385 |
+
if K > 0 and pred_bits is not None:
|
| 386 |
+
kj = int(k_budget[j]) - 1
|
| 387 |
+
def _digs(bitrow):
|
| 388 |
+
return "".join(str(d + 1) for d in range(9) if bitrow[d])
|
| 389 |
+
cand_shown = []
|
| 390 |
+
for cell in range(81):
|
| 391 |
+
if cand_targets[j, kj, cell] <= 0: # clue / not supervised
|
| 392 |
+
continue
|
| 393 |
+
r, c = cell // 9, cell % 9
|
| 394 |
+
pset = _digs(pred_bits[j, kj, cell])
|
| 395 |
+
tset = _digs(tgt_bits[j, kj, cell])
|
| 396 |
+
cand_shown.append(f"(r{r},c{c}) pred{{{pset}}} true{{{tset}}}")
|
| 397 |
+
if len(cand_shown) >= 8:
|
| 398 |
+
break
|
| 399 |
+
print(f"EXAMPLE candidate-set @slot{kj} (pred vs true):",
|
| 400 |
+
" ".join(cand_shown), flush=True)
|
| 401 |
+
|
| 402 |
+
correct_eval_sudoku_puzzle = 0
|
| 403 |
+
|
| 404 |
+
for i in range(len(cur_input_seq)):
|
| 405 |
+
|
| 406 |
+
# increase correct_eval_sudoku_puzzle when the model output solution
|
| 407 |
+
# for a given puzzle is correct
|
| 408 |
+
stripped = strip_latent_slots(cur_input_seq[i], int(start_index[i, 0]))
|
| 409 |
+
correct_eval_sudoku_puzzle += valid_solution(stripped)
|
| 410 |
+
|
| 411 |
+
eval_metrics["acc_complete_puzzle"].append(
|
| 412 |
+
correct_eval_sudoku_puzzle * 1.0 / len(cur_input_seq)
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
per_level = {lvl: (level_ok[lvl] / level_tot[lvl] if level_tot[lvl] else -1.0)
|
| 416 |
+
for lvl in range(3, 9)}
|
| 417 |
+
eval_metrics["per_level_acc"] = per_level
|
| 418 |
+
print("PER-LEVEL cell acc:",
|
| 419 |
+
{lvl: (f"{v:.3f}" if v >= 0 else "n/a") for lvl, v in per_level.items()},
|
| 420 |
+
flush=True)
|
| 421 |
+
|
| 422 |
+
# Per-depth candidate-set accuracy, keyed by stage (slot j -> stage j+1) so
|
| 423 |
+
# the curriculum controller can index it directly by stage number.
|
| 424 |
+
per_slot = {j + 1: (float(slot_ok[j] / slot_tot[j]) if slot_tot[j] else -1.0)
|
| 425 |
+
for j in range(K)}
|
| 426 |
+
per_slot_ch = {j + 1: (float(slot_ok_ch[j] / slot_tot_ch[j])
|
| 427 |
+
if slot_tot_ch[j] else -1.0) for j in range(K)}
|
| 428 |
+
eval_metrics["per_slot_acc"] = per_slot
|
| 429 |
+
eval_metrics["per_slot_acc_changed"] = per_slot_ch
|
| 430 |
+
|
| 431 |
+
# Keyed by stage (slot s -> stage s+1) to match per_slot_acc.
|
| 432 |
+
per_stage_inset = {s + 1: (float(inset_ok[s] / inset_tot[s])
|
| 433 |
+
if inset_tot[s] else -1.0) for s in range(K)}
|
| 434 |
+
eval_metrics["per_stage_inset_acc"] = per_stage_inset
|
| 435 |
+
if K > 0 and any(v >= 0 for v in per_stage_inset.values()):
|
| 436 |
+
print("PER-STAGE in-set rate (emitted digit is a stage-s candidate):",
|
| 437 |
+
{s: (f"{v:.3f}" if v >= 0 else "n/a")
|
| 438 |
+
for s, v in per_stage_inset.items()}, flush=True)
|
| 439 |
+
|
| 440 |
+
per_bin = {b: (bin_ok[b] / bin_tot[b] if bin_tot[b] else -1.0)
|
| 441 |
+
for b in range(1, n_bins + 1)}
|
| 442 |
+
eval_metrics["per_bin_acc"] = per_bin
|
| 443 |
+
if any(v >= 0 for v in per_bin.values()):
|
| 444 |
+
print("PER-ROUND-BIN cell acc:",
|
| 445 |
+
{b: (f"{v:.3f}" if v >= 0 else "n/a") for b, v in per_bin.items()},
|
| 446 |
+
flush=True)
|
| 447 |
+
if K > 0:
|
| 448 |
+
print("PER-DEPTH cand-set acc:",
|
| 449 |
+
{s: (f"{v:.3f}" if v >= 0 else "n/a") for s, v in per_slot.items()},
|
| 450 |
+
flush=True)
|
| 451 |
+
print("PER-DEPTH cand-set acc (changed cells only):",
|
| 452 |
+
{s: (f"{v:.3f}" if v >= 0 else "n/a")
|
| 453 |
+
for s, v in per_slot_ch.items()}, flush=True)
|
| 454 |
+
|
| 455 |
+
return eval_metrics
|
code/train/main.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
config.promote_acc_threshold = float(os.environ.get("SUDOKU_PROMOTE_ACC", 0.85))
|
| 155 |
+
# Instance arm: inset is only scored on location-correct cells. Without a
|
| 156 |
+
# location floor, a 0.90 inset on 2% loc_acc would promote from a handful
|
| 157 |
+
# of cells. Stage-0 chance inset is ~3.7/9 ≈ 0.41, so 0.85 is the set bar
|
| 158 |
+
# and this is the "model actually emits the solver-order cells" bar.
|
| 159 |
+
config.promote_loc_threshold = float(os.environ.get("SUDOKU_PROMOTE_LOC", 0.70))
|
| 160 |
+
config.promote_patience_steps = int(os.environ.get("SUDOKU_PATIENCE", 8000))
|
| 161 |
+
config.min_stage_steps = int(os.environ.get("SUDOKU_MIN_STAGE_STEPS", 2000))
|
| 162 |
+
# Plateau promotion: advance once the frontier depth stops improving, rather
|
| 163 |
+
# than on a fixed timer. A stage is "done" when it has not gained
|
| 164 |
+
# plateau_delta over its best accuracy for plateau_steps steps. This is the
|
| 165 |
+
# primary rule; the accuracy threshold is a fast path for mastery and
|
| 166 |
+
# promote_patience_steps is a hard cap so a stuck stage cannot stall
|
| 167 |
+
# training forever. Set plateau_steps=0 to disable.
|
| 168 |
+
config.plateau_steps = int(os.environ.get("SUDOKU_PLATEAU_STEPS", 20000))
|
| 169 |
+
config.plateau_delta = float(os.environ.get("SUDOKU_PLATEAU_DELTA", 0.005))
|
| 170 |
+
# Train-time difficulty balancing. 0 (default) = draw puzzles uniformly from
|
| 171 |
+
# the corpus, so the difficulty tag selects nothing. 1 = uniform over the 6
|
| 172 |
+
# levels, which upsamples level 8 from 1.8% to 16.7% of batches. Eval is
|
| 173 |
+
# always level-balanced so per-level accuracy stays measurable.
|
| 174 |
+
config.level_balanced_sampling = int(
|
| 175 |
+
os.environ.get("SUDOKU_LEVEL_BALANCED", 0))
|
| 176 |
+
# Data curriculum over the puzzle POOL (as opposed to the latent-depth
|
| 177 |
+
# curriculum over the supervision).
|
| 178 |
+
# "none" = every puzzle available from step 0.
|
| 179 |
+
# "rounds" = stage t admits only puzzles whose solver round count falls in
|
| 180 |
+
# the first t of max_stage equal-count bins (rounds span 5..38,
|
| 181 |
+
# so 12 bins give a genuinely smooth 12-step ladder). This is
|
| 182 |
+
# the same propagation-depth axis the latent arm supervises,
|
| 183 |
+
# which makes the two arms directly comparable. Needs
|
| 184 |
+
# SUDOKU_TRAIN_META / SUDOKU_TEST_META.
|
| 185 |
+
config.data_curriculum = os.environ.get("SUDOKU_DATA_CURRICULUM", "none")
|
| 186 |
+
config.train_meta_path = os.environ.get("SUDOKU_TRAIN_META", "") or None
|
| 187 |
+
config.test_meta_path = os.environ.get("SUDOKU_TEST_META", "") or None
|
| 188 |
+
|
| 189 |
+
# Model related parameters
|
| 190 |
+
config.block_size = 81
|
| 191 |
+
config.seq_len = 3 * config.block_size + config.num_latent_slots
|
| 192 |
+
config.vocab_size = 11
|
| 193 |
+
|
| 194 |
+
# Model architecture
|
| 195 |
+
config.num_heads = 8
|
| 196 |
+
config.num_layers = 8
|
| 197 |
+
config.emb_dim = 576
|
| 198 |
+
config.qkv_dim = 576
|
| 199 |
+
config.mlp_dim = 6 * config.emb_dim
|
| 200 |
+
config.dropout_rate = float(os.environ.get("SUDOKU_DROPOUT", 0.2))
|
| 201 |
+
config.attention_dropout_rate = float(
|
| 202 |
+
os.environ.get("SUDOKU_ATTN_DROPOUT",
|
| 203 |
+
os.environ.get("SUDOKU_DROPOUT", 0.2)))
|
| 204 |
+
|
| 205 |
+
# Training hyperparameters
|
| 206 |
+
config.learning_rate = float(os.environ.get("SUDOKU_LR", 0.0002)) # Base learning rate.
|
| 207 |
+
config.end_lr_factor = float(os.environ.get("SUDOKU_END_LR_FACTOR", 0.2))
|
| 208 |
+
config.warmup_tokens = int(os.environ.get("SUDOKU_WARMUP", 10000))
|
| 209 |
+
config.weight_decay = float(os.environ.get("SUDOKU_WD", 0.005))
|
| 210 |
+
# Resume from a checkpoint (set SUDOKU_RESUME=1 and pass --ckpt_loc=<path>).
|
| 211 |
+
config.resume_training = os.environ.get("SUDOKU_RESUME", "0") == "1"
|
| 212 |
+
|
| 213 |
+
# Other hyperparameters
|
| 214 |
+
config.seed = 7
|
| 215 |
+
config.save_checkpoint = os.environ.get("SUDOKU_SAVE_CKPT", "1") == "1"
|
| 216 |
+
config.save_every_steps = int(os.environ.get("SUDOKU_SAVE_EVERY", 10000))
|
| 217 |
+
# How many checkpoints to retain. Large default so per-stage checkpoints are
|
| 218 |
+
# never rolled off (disk is plentiful; ~0.5GB each).
|
| 219 |
+
config.ckpt_keep = int(os.environ.get("SUDOKU_CKPT_KEEP", 100))
|
| 220 |
+
config.use_wandb = False
|
| 221 |
+
config.wandb_project_name = 'sudoku'
|
| 222 |
+
|
| 223 |
+
# Evaluation related parameters
|
| 224 |
+
config.eval_every_steps = int(os.environ.get("SUDOKU_EVAL_EVERY", 2000))
|
| 225 |
+
config.eval_epochs = int(os.environ.get("SUDOKU_EVAL_EPOCHS", 5))
|
| 226 |
+
|
| 227 |
+
# Path to dataset
|
| 228 |
+
config.train_puzzle_path = os.environ.get(
|
| 229 |
+
"SUDOKU_TRAIN_PATH", "datasets/train_sudoku_puzzles.npy")
|
| 230 |
+
config.train_candidate_path = "datasets/train_sudoku_puzzles_candidate.npy"
|
| 231 |
+
config.test_puzzle_path = os.environ.get(
|
| 232 |
+
"SUDOKU_TEST_PATH", "datasets/test_sudoku_puzzles.npy")
|
| 233 |
+
config.test_candidate_path = "datasets/test_sudoku_puzzles_candidate.npy"
|
| 234 |
+
|
| 235 |
+
# Staged multi-candidate supervision (per-latent-slot BCE targets).
|
| 236 |
+
# Empty string disables cand-mask loading (useful for K=0 baselines).
|
| 237 |
+
config.train_cand_masks_path = os.environ.get(
|
| 238 |
+
"SUDOKU_TRAIN_CAND", "datasets_multicandidate/train_cand_masks.npy") or None
|
| 239 |
+
config.test_cand_masks_path = os.environ.get(
|
| 240 |
+
"SUDOKU_TEST_CAND", "datasets_multicandidate/test_cand_masks.npy") or None
|
| 241 |
+
# Superposition-instance targets. When set, the output prompt's value tokens
|
| 242 |
+
# come from one sampled stage-k assignment instead of the unique solution:
|
| 243 |
+
# the input prompt (clues) is fixed and the same puzzle recurs with different
|
| 244 |
+
# legal completions, so the candidate set is represented across the batch
|
| 245 |
+
# rather than supervised as a multi-hot set. Setting this should go with
|
| 246 |
+
# SUDOKU_AUX_WEIGHT=0 (candidate head off) -- the masks are then read only
|
| 247 |
+
# for the in-set metric. Empty string = classic single-solution targets.
|
| 248 |
+
config.instance_dir = os.environ.get("SUDOKU_INSTANCE_DIR", "") or None
|
| 249 |
+
# Weight of the auxiliary candidate-set BCE loss relative to the LM CE loss.
|
| 250 |
+
config.aux_cand_weight = float(os.environ.get("SUDOKU_AUX_WEIGHT", 1.0))
|
| 251 |
+
# Positive-class weight inside the candidate BCE (counters the sparsity of
|
| 252 |
+
# the multi-hot masks so the head doesn't collapse to predicting all-zeros).
|
| 253 |
+
config.aux_pos_weight = float(os.environ.get("SUDOKU_CAND_POS_WEIGHT", 5.0))
|
| 254 |
+
|
| 255 |
+
# How many latent slots each example activates.
|
| 256 |
+
# "level" = k = clip(level-2, 1, K). Difficulty-matched, but puzzle level
|
| 257 |
+
# explains only ~19% of the variance in solver round count, so
|
| 258 |
+
# most examples leave the majority of the K slots inert (a
|
| 259 |
+
# level-3 puzzle activates ONE slot for ~21 rounds of work).
|
| 260 |
+
# "depth" = k = num_passes, uniform over the batch. Every slot the
|
| 261 |
+
# recurrence actually fills is active and supervised, and the
|
| 262 |
+
# curriculum advances reasoning depth rather than puzzle level.
|
| 263 |
+
config.cand_slot_mode = os.environ.get("SUDOKU_CAND_SLOT_MODE", "depth")
|
| 264 |
+
# Latent passes granted per curriculum stage: num_passes = min(pps*stage, K).
|
| 265 |
+
# pps=2 with K=12 reaches all 12 slots by stage 6.
|
| 266 |
+
config.passes_per_stage = int(os.environ.get("SUDOKU_PASSES_PER_STAGE", 1))
|
| 267 |
+
# Loss weight for candidate cells that did NOT change from the previous
|
| 268 |
+
# stage. Consecutive stages are highly redundant (at 12 stages ~97% of the
|
| 269 |
+
# target bits are copies of the previous slot), so plain BCE is dominated by
|
| 270 |
+
# echoing the previous slot. <1.0 down-weights the copied cells and puts the
|
| 271 |
+
# gradient on the digits actually eliminated at this stage. 1.0 = off.
|
| 272 |
+
config.aux_delta_bg = float(os.environ.get("SUDOKU_CAND_DELTA_BG", 1.0))
|
| 273 |
+
|
| 274 |
+
return config
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def main(argv):
|
| 278 |
+
if len(argv) > 1:
|
| 279 |
+
raise app.UsageError('Too many command-line arguments.')
|
| 280 |
+
|
| 281 |
+
# # Hide any GPUs from TensorFlow. Otherwise TF might reserve memory and make
|
| 282 |
+
# # it unavailable to JAX.
|
| 283 |
+
tf.config.experimental.set_visible_devices([], 'GPU')
|
| 284 |
+
|
| 285 |
+
cfgs = get_config()
|
| 286 |
+
if cfgs.resume_training:
|
| 287 |
+
assert _CKPT_LOC.value is not None
|
| 288 |
+
|
| 289 |
+
if cfgs.use_wandb:
|
| 290 |
+
wandb.init(project=cfgs.wandb_project_name, name=_EXP_NAME.value, config=cfgs)
|
| 291 |
+
|
| 292 |
+
logging.info('JAX process: %d / %d', jax.process_index(), jax.process_count())
|
| 293 |
+
logging.info('JAX local devices: %r', jax.local_devices())
|
| 294 |
+
|
| 295 |
+
# Add a note so that we can tell which task is which JAX host.
|
| 296 |
+
# (Depending on the platform task 0 is not guaranteed to be host 0)
|
| 297 |
+
platform.work_unit().set_task_status(f'process_index: {jax.process_index()}, '
|
| 298 |
+
f'process_count: {jax.process_count()}')
|
| 299 |
+
platform.work_unit().create_artifact(platform.ArtifactType.DIRECTORY,
|
| 300 |
+
_WORKDIR.value, 'workdir')
|
| 301 |
+
|
| 302 |
+
logging.info(cfgs)
|
| 303 |
+
|
| 304 |
+
cfgs.workdir = _WORKDIR.value
|
| 305 |
+
cfgs.ckpt_loc = _CKPT_LOC.value
|
| 306 |
+
if int(getattr(cfgs, "backtrack", 0)):
|
| 307 |
+
if str(getattr(cfgs, "backtrack_mode", "prob")) == "adaptive":
|
| 308 |
+
train_backtrack.train_and_evaluate_backtrack_adaptive(cfgs, _WORKDIR.value)
|
| 309 |
+
else:
|
| 310 |
+
train_backtrack.train_and_evaluate_backtrack(cfgs, _WORKDIR.value)
|
| 311 |
+
else:
|
| 312 |
+
train_and_evaluate.train_and_evaluate(cfgs, _WORKDIR.value)
|
| 313 |
+
|
| 314 |
+
if cfgs.use_wandb:
|
| 315 |
+
wandb.finish()
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
if __name__ == '__main__':
|
| 319 |
+
jax.config.config_with_absl()
|
| 320 |
+
app.run(main)
|
code/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/train/train_and_evaluate.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 math
|
| 20 |
+
import os
|
| 21 |
+
|
| 22 |
+
from absl import logging
|
| 23 |
+
from clu import metric_writers
|
| 24 |
+
from flax import jax_utils
|
| 25 |
+
from flax import linen as nn
|
| 26 |
+
from flax.training import checkpoints
|
| 27 |
+
import jax
|
| 28 |
+
from jax import random
|
| 29 |
+
import jax.numpy as jnp
|
| 30 |
+
import numpy as np
|
| 31 |
+
import tensorflow as tf
|
| 32 |
+
import wandb
|
| 33 |
+
|
| 34 |
+
from train import data
|
| 35 |
+
from train import evaluater
|
| 36 |
+
from train import model
|
| 37 |
+
from train import trainer
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def log_hyperparams_tb(
|
| 42 |
+
config, model_config, initial_variables, tf_summary_writer
|
| 43 |
+
):
|
| 44 |
+
"""Log hyperparameters to TensorBoard.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
config: experiment's ConfigDict
|
| 48 |
+
model_config: model's ConfigDict
|
| 49 |
+
initial_variables: initial hyperparameter values
|
| 50 |
+
tf_summary_writer: SummaryWriter object.
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
The SummaryWriter object and the config.
|
| 54 |
+
"""
|
| 55 |
+
# Calculate the total number of model parameters
|
| 56 |
+
config.num_model_parameters = sum(
|
| 57 |
+
x.size for x in jax.tree_util.tree_leaves(initial_variables)
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
# Convert hyperparameters to tensors
|
| 61 |
+
config_hyperparameters = [
|
| 62 |
+
tf.convert_to_tensor([k, str(v)]) for k, v in config.items()
|
| 63 |
+
]
|
| 64 |
+
model_config_hyperparameters = [
|
| 65 |
+
tf.convert_to_tensor([k, str(v)])
|
| 66 |
+
for k, v in model_config.__dict__.items()
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
# Log model hyperparameters to TensorBoard
|
| 70 |
+
with tf_summary_writer.as_default():
|
| 71 |
+
tf.summary.text(
|
| 72 |
+
"Model hyperparameters", tf.stack(model_config_hyperparameters), step=0
|
| 73 |
+
)
|
| 74 |
+
tf.summary.text(
|
| 75 |
+
"Config hyperparameters", tf.stack(config_hyperparameters), step=0
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
return tf_summary_writer, config
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def train_and_evaluate(config, workdir):
|
| 83 |
+
"""The training and evaluation loops for the model.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
config: experiment's config dictionary.
|
| 87 |
+
workdir: directory to use for logging.
|
| 88 |
+
"""
|
| 89 |
+
# Orbax checkpointing requires an absolute path.
|
| 90 |
+
workdir = os.path.abspath(workdir)
|
| 91 |
+
|
| 92 |
+
logging.info("Creating training and evaluator dataset iterator")
|
| 93 |
+
curriculum = data.CurriculumState(
|
| 94 |
+
stage=int(getattr(config, "curriculum_start_stage", 1)),
|
| 95 |
+
max_stage=int(getattr(config, "curriculum_max_stage", 6)))
|
| 96 |
+
train_data_iter = data.create_iter(
|
| 97 |
+
config, config.minibatch_size, train=True, curriculum=curriculum)
|
| 98 |
+
eval_data_iter = data.create_iter(config, config.minibatch_size, train=False)
|
| 99 |
+
|
| 100 |
+
logging.info("Finished creating training dataset iterator")
|
| 101 |
+
|
| 102 |
+
model_config = model.TransformerConfig(
|
| 103 |
+
dtype=config.dtype,
|
| 104 |
+
vocab_size=config.vocab_size,
|
| 105 |
+
seq_len=config.seq_len,
|
| 106 |
+
num_heads=config.num_heads,
|
| 107 |
+
num_layers=config.num_layers,
|
| 108 |
+
emb_dim=config.emb_dim,
|
| 109 |
+
qkv_dim=config.qkv_dim,
|
| 110 |
+
mlp_dim=config.mlp_dim,
|
| 111 |
+
dropout_rate=config.dropout_rate,
|
| 112 |
+
attention_dropout_rate=config.attention_dropout_rate,
|
| 113 |
+
deterministic=False,
|
| 114 |
+
num_latent_slots=int(config.num_latent_slots),
|
| 115 |
+
inject_latents=bool(int(getattr(config, "recurrent_latent", 1))),
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
logging.info("train_config: %s", str(model_config.__dict__))
|
| 119 |
+
print(str(model_config.__dict__), flush=True)
|
| 120 |
+
|
| 121 |
+
rng = jax.random.PRNGKey(config.seed)
|
| 122 |
+
rng, init_rng, inference_rng = random.split(rng, num=3)
|
| 123 |
+
|
| 124 |
+
# Initialize the model and get initial variables. Dummy latent arguments
|
| 125 |
+
# are provided so the latent projector parameters are created at init.
|
| 126 |
+
rng, dropout_rng = jax.random.split(rng)
|
| 127 |
+
input_shape = (config.minibatch_size, config.seq_len)
|
| 128 |
+
net = model.TransformerLMHeadModel(model_config)
|
| 129 |
+
rng_keys = {"params": init_rng, "dropout": dropout_rng}
|
| 130 |
+
K = int(config.num_latent_slots)
|
| 131 |
+
dummy_latents = jnp.zeros(
|
| 132 |
+
(config.minibatch_size, K, config.emb_dim), model_config.dtype)
|
| 133 |
+
dummy_positions = jnp.zeros((config.minibatch_size, K), jnp.int32)
|
| 134 |
+
dummy_active = jnp.zeros((config.minibatch_size, K), bool)
|
| 135 |
+
sample_out, initial_variables = jax.jit(
|
| 136 |
+
net.init_with_output
|
| 137 |
+
)(rng_keys, jnp.ones(input_shape, jnp.int32), dummy_latents,
|
| 138 |
+
dummy_positions, dummy_active)
|
| 139 |
+
|
| 140 |
+
state, lr_scheduler_fn = trainer.get_state(config, net, initial_variables)
|
| 141 |
+
# Resume-and-extend support: when resuming, start the training loop at the
|
| 142 |
+
# restored optimizer step (not 0) so a larger config.max_steps continues the
|
| 143 |
+
# cosine LR schedule cleanly instead of re-running steps or overshooting.
|
| 144 |
+
start_step = 0
|
| 145 |
+
if config.resume_training:
|
| 146 |
+
state = checkpoints.restore_checkpoint(config.ckpt_loc, state)
|
| 147 |
+
start_step = int(state.step)
|
| 148 |
+
print("----------Restored model from", config.ckpt_loc,
|
| 149 |
+
f"at step {start_step}-----------")
|
| 150 |
+
|
| 151 |
+
writer = metric_writers.create_default_writer(
|
| 152 |
+
workdir, asynchronous=False, just_logging=(jax.process_index() > 0))
|
| 153 |
+
tf_summary_writer = tf.summary.create_file_writer(workdir)
|
| 154 |
+
|
| 155 |
+
logging.info("config: %s", str(config.__dict__))
|
| 156 |
+
state = jax_utils.replicate(state)
|
| 157 |
+
|
| 158 |
+
dropout_rngs = jax.random.split(rng, jax.local_device_count())
|
| 159 |
+
|
| 160 |
+
def make_p_train_step(num_passes):
|
| 161 |
+
return jax.pmap(
|
| 162 |
+
functools.partial(
|
| 163 |
+
trainer.train_step,
|
| 164 |
+
config=model_config,
|
| 165 |
+
hyperparams=config,
|
| 166 |
+
learning_rate_fn=lr_scheduler_fn,
|
| 167 |
+
num_passes=num_passes),
|
| 168 |
+
axis_name="batch",
|
| 169 |
+
donate_argnums=(0,))
|
| 170 |
+
|
| 171 |
+
# num_passes = latent recurrence depth for this stage, capped by the number
|
| 172 |
+
# of latent slots (K=0 -> 0 passes -> no-latent control baseline).
|
| 173 |
+
# When recurrence is disabled (control), force 0 passes so no hidden state
|
| 174 |
+
# is ever fed back: the slots become static, independent per-stage readouts.
|
| 175 |
+
recurrent = bool(int(getattr(config, "recurrent_latent", 1)))
|
| 176 |
+
passes_per_stage = int(getattr(config, "passes_per_stage", 1))
|
| 177 |
+
def passes_for(stage):
|
| 178 |
+
return min(passes_per_stage * stage, K) if recurrent else 0
|
| 179 |
+
p_train_step = make_p_train_step(passes_for(curriculum.stage))
|
| 180 |
+
|
| 181 |
+
p_eval_step = jax.pmap(functools.partial(evaluater.eval_step,
|
| 182 |
+
config=model_config.replace(deterministic=True)),
|
| 183 |
+
axis_name="batch")
|
| 184 |
+
|
| 185 |
+
hooks, report_progress, train_metrics = trainer.get_metrics_report_progress(
|
| 186 |
+
config, workdir, writer)
|
| 187 |
+
|
| 188 |
+
tf_summary_writer, config = log_hyperparams_tb(
|
| 189 |
+
config, model_config, initial_variables, tf_summary_writer
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
promote_threshold = float(getattr(config, "promote_acc_threshold", 0.85))
|
| 193 |
+
promote_loc_threshold = float(getattr(config, "promote_loc_threshold", 0.70))
|
| 194 |
+
promote_patience = int(getattr(config, "promote_patience_steps", 8000))
|
| 195 |
+
min_stage_steps = int(getattr(config, "min_stage_steps", 2000))
|
| 196 |
+
plateau_steps = int(getattr(config, "plateau_steps", 0))
|
| 197 |
+
plateau_delta = float(getattr(config, "plateau_delta", 0.005))
|
| 198 |
+
instance_mode = bool(getattr(config, "instance_dir", None))
|
| 199 |
+
per_stage_inset = {}
|
| 200 |
+
# Best frontier-depth accuracy seen in the current stage, and the step it
|
| 201 |
+
# was last improved: the plateau detector's state.
|
| 202 |
+
stage_best_acc = -1.0
|
| 203 |
+
stage_best_step = start_step
|
| 204 |
+
ckpt_keep = int(getattr(config, "ckpt_keep", 100))
|
| 205 |
+
# Protected directory for per-stage checkpoints (never rolled off).
|
| 206 |
+
stage_ckpt_dir = os.path.join(workdir, "stage_ckpts")
|
| 207 |
+
stage_started_at = start_step
|
| 208 |
+
|
| 209 |
+
with metric_writers.ensure_flushes(writer):
|
| 210 |
+
for step in range(start_step, config.max_steps):
|
| 211 |
+
if step%10000 == 0:
|
| 212 |
+
print("Step:", step, flush=True)
|
| 213 |
+
|
| 214 |
+
state, metrics = trainer.train_one_step(p_train_step, config, state,
|
| 215 |
+
step, dropout_rngs, train_data_iter)
|
| 216 |
+
|
| 217 |
+
for h in hooks:
|
| 218 |
+
h(step)
|
| 219 |
+
|
| 220 |
+
if math.isnan(metrics["loss"][0]):
|
| 221 |
+
print("The loss function became nan: This might be due to the choice of hyperparameters.")
|
| 222 |
+
break
|
| 223 |
+
|
| 224 |
+
if step % config.eval_every_steps == 0:
|
| 225 |
+
eval_metrics = evaluater.get_eval_metrics(
|
| 226 |
+
state, eval_data_iter, p_eval_step, config)
|
| 227 |
+
per_level = eval_metrics.pop("per_level_acc")
|
| 228 |
+
per_slot = eval_metrics.pop("per_slot_acc", {})
|
| 229 |
+
per_slot_ch = eval_metrics.pop("per_slot_acc_changed", {})
|
| 230 |
+
per_stage_inset = eval_metrics.pop("per_stage_inset_acc", {})
|
| 231 |
+
per_bin = eval_metrics.pop("per_bin_acc", {})
|
| 232 |
+
def _m(key):
|
| 233 |
+
v = eval_metrics.get(key, [])
|
| 234 |
+
return round(float(np.mean(v)), 4) if len(v) else -1.0
|
| 235 |
+
print(step,
|
| 236 |
+
"stage", curriculum.stage,
|
| 237 |
+
"loss", round(float(metrics["loss"].mean()), 4),
|
| 238 |
+
"ce", round(float(metrics["ce_loss"].mean()), 4),
|
| 239 |
+
"aux_bce", round(float(metrics["aux_loss"].mean()), 4),
|
| 240 |
+
"| val_acc", _m("acc"),
|
| 241 |
+
"loc_acc", _m("loc_acc"),
|
| 242 |
+
"val|loc", _m("val_given_loc_acc"),
|
| 243 |
+
"| cand_bit_acc", _m("cand_bit_acc"),
|
| 244 |
+
"cand_set_acc", _m("cand_set_acc"),
|
| 245 |
+
"cand_set_chg", _m("cand_set_acc_changed"),
|
| 246 |
+
"| inset", round(
|
| 247 |
+
float(per_stage_inset.get(curriculum.stage, -1.0)), 4),
|
| 248 |
+
"promote_need inset>={:.2f} loc>={:.2f}".format(
|
| 249 |
+
promote_threshold, promote_loc_threshold),
|
| 250 |
+
"| depth_acc", round(
|
| 251 |
+
float(per_slot_ch.get(curriculum.stage, -1.0)), 4),
|
| 252 |
+
flush=True)
|
| 253 |
+
with tf_summary_writer.as_default():
|
| 254 |
+
tf.summary.scalar("loss", metrics["loss"].mean(), step=step)
|
| 255 |
+
tf.summary.scalar("ce_loss", metrics["ce_loss"].mean(), step=step)
|
| 256 |
+
tf.summary.scalar("aux_bce_loss", metrics["aux_loss"].mean(), step=step)
|
| 257 |
+
tf.summary.scalar(
|
| 258 |
+
"learning rate", metrics["learning_rate"].mean(), step=step
|
| 259 |
+
)
|
| 260 |
+
tf.summary.scalar("curriculum_stage", curriculum.stage, step=step)
|
| 261 |
+
|
| 262 |
+
log_dict = {'loss': metrics["loss"].mean(), 'learning rate': metrics["learning_rate"].mean()}
|
| 263 |
+
|
| 264 |
+
for key in eval_metrics.keys():
|
| 265 |
+
vals = eval_metrics[key]
|
| 266 |
+
if not vals:
|
| 267 |
+
continue
|
| 268 |
+
tf.summary.scalar(
|
| 269 |
+
"eval_" + key, np.array(vals).mean(), step=step
|
| 270 |
+
)
|
| 271 |
+
log_dict[ "eval_" + key ] = np.array(vals).mean()
|
| 272 |
+
|
| 273 |
+
for lvl, v in per_level.items():
|
| 274 |
+
if v >= 0:
|
| 275 |
+
tf.summary.scalar(f"eval_acc_level_{lvl}", v, step=step)
|
| 276 |
+
log_dict[f"eval_acc_level_{lvl}"] = v
|
| 277 |
+
|
| 278 |
+
for s, v in per_slot.items():
|
| 279 |
+
if v >= 0:
|
| 280 |
+
tf.summary.scalar(f"eval_cand_depth_{s}", v, step=step)
|
| 281 |
+
log_dict[f"eval_cand_depth_{s}"] = v
|
| 282 |
+
for s, v in per_slot_ch.items():
|
| 283 |
+
if v >= 0:
|
| 284 |
+
tf.summary.scalar(f"eval_cand_depth_chg_{s}", v, step=step)
|
| 285 |
+
|
| 286 |
+
if config.use_wandb: wandb.log(log_dict, step=step)
|
| 287 |
+
|
| 288 |
+
# ---- Curriculum promotion (ATC-style: threshold + patience) ----
|
| 289 |
+
# Two curricula, one frontier rule. With latent slots the
|
| 290 |
+
# frontier is the DEEPEST active slot, i.e. the newest wave
|
| 291 |
+
# snapshot this stage introduced, scored on changed cells only
|
| 292 |
+
# (unchanged cells are copies of slot j-1 and stay correct for a
|
| 293 |
+
# head that learned nothing new). With the round-count data
|
| 294 |
+
# curriculum the frontier is the newest round-BIN, i.e. the
|
| 295 |
+
# longest-chain puzzles this stage admitted.
|
| 296 |
+
rounds_curric = str(
|
| 297 |
+
getattr(config, "data_curriculum", "none")) == "rounds"
|
| 298 |
+
has_frontier = int(config.num_latent_slots) > 0 or rounds_curric
|
| 299 |
+
if has_frontier and curriculum.stage < curriculum.max_stage:
|
| 300 |
+
t = curriculum.stage
|
| 301 |
+
loc_now = _m("loc_acc")
|
| 302 |
+
if instance_mode:
|
| 303 |
+
# Inset = "emitted digit is in the stage-t set", scored
|
| 304 |
+
# only on location-correct cells. Require BOTH: the
|
| 305 |
+
# solver-order locations, and values inside the set.
|
| 306 |
+
# Otherwise a high inset on ~2% loc_acc promotes from
|
| 307 |
+
# noise. Plateau is also blocked until location is up,
|
| 308 |
+
# so we cannot skip stage 1 the way the old patience
|
| 309 |
+
# promotions did at 0.32-0.46.
|
| 310 |
+
inset_now = per_stage_inset.get(t, -1.0)
|
| 311 |
+
frontier_acc = float(inset_now) if inset_now >= 0 else -1.0
|
| 312 |
+
elif int(config.num_latent_slots) > 0:
|
| 313 |
+
frontier_acc = per_slot_ch.get(t, -1.0)
|
| 314 |
+
if frontier_acc < 0:
|
| 315 |
+
frontier_acc = per_slot.get(t, -1.0)
|
| 316 |
+
else:
|
| 317 |
+
frontier_acc = per_bin.get(t, -1.0)
|
| 318 |
+
if frontier_acc < 0:
|
| 319 |
+
# The frontier bin holds as little as 6.5% of the
|
| 320 |
+
# corpus, so a single eval can miss it. Fall back to
|
| 321 |
+
# the pooled accuracy over everything unlocked.
|
| 322 |
+
seen = [v for b, v in per_bin.items()
|
| 323 |
+
if b <= t and v >= 0]
|
| 324 |
+
frontier_acc = (float(np.mean(seen)) if seen
|
| 325 |
+
else -1.0)
|
| 326 |
+
steps_in_stage = step - stage_started_at
|
| 327 |
+
# A negative accuracy means "not measured this eval". Never
|
| 328 |
+
# let that count as evidence: it must not reset the plateau
|
| 329 |
+
# tracker, and it must not satisfy the threshold or plateau
|
| 330 |
+
# rule. Only the hard patience cap can fire without a
|
| 331 |
+
# measurement.
|
| 332 |
+
measured = frontier_acc >= 0
|
| 333 |
+
if measured and frontier_acc > stage_best_acc + plateau_delta:
|
| 334 |
+
stage_best_acc = frontier_acc
|
| 335 |
+
stage_best_step = step
|
| 336 |
+
loc_ready = (not instance_mode) or loc_now >= promote_loc_threshold
|
| 337 |
+
hit_threshold = (measured and loc_ready
|
| 338 |
+
and frontier_acc >= promote_threshold)
|
| 339 |
+
stalled = (measured and loc_ready and plateau_steps > 0
|
| 340 |
+
and (step - stage_best_step) >= plateau_steps)
|
| 341 |
+
patience_over = steps_in_stage >= promote_patience
|
| 342 |
+
if steps_in_stage >= min_stage_steps and (
|
| 343 |
+
hit_threshold or stalled or patience_over):
|
| 344 |
+
reason = ("threshold" if hit_threshold
|
| 345 |
+
else "plateau" if stalled else "patience")
|
| 346 |
+
curriculum.stage += 1
|
| 347 |
+
stage_started_at = step
|
| 348 |
+
stage_best_acc = -1.0
|
| 349 |
+
stage_best_step = step
|
| 350 |
+
p_train_step = make_p_train_step(passes_for(curriculum.stage))
|
| 351 |
+
what = ("round-bin" if int(config.num_latent_slots) == 0
|
| 352 |
+
else "depth")
|
| 353 |
+
print(f"[curriculum] step {step}: promote to stage "
|
| 354 |
+
f"{curriculum.stage} ({reason}; graduated {what} "
|
| 355 |
+
f"{t} acc={frontier_acc:.3f} after "
|
| 356 |
+
f"{steps_in_stage} steps); "
|
| 357 |
+
f"latent passes={curriculum.stage}, "
|
| 358 |
+
f"pool/snapshots now 1..{curriculum.stage}",
|
| 359 |
+
flush=True)
|
| 360 |
+
if config.save_checkpoint:
|
| 361 |
+
unrep_state = jax_utils.unreplicate(state)
|
| 362 |
+
# Rolling checkpoint in the main workdir.
|
| 363 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 364 |
+
workdir, unrep_state, step,
|
| 365 |
+
keep=ckpt_keep, overwrite=True)
|
| 366 |
+
# Protected copy: the model *entering* this stage,
|
| 367 |
+
# kept permanently under stage_ckpts/ (never rolled
|
| 368 |
+
# off), so every stage's checkpoint survives.
|
| 369 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 370 |
+
stage_ckpt_dir, unrep_state, step,
|
| 371 |
+
keep=100, overwrite=True,
|
| 372 |
+
prefix=f"stage{curriculum.stage}_")
|
| 373 |
+
|
| 374 |
+
if config.save_checkpoint and step > 0 and step % config.save_every_steps == 0:
|
| 375 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 376 |
+
workdir, jax_utils.unreplicate(state), step,
|
| 377 |
+
keep=ckpt_keep, overwrite=True
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
# Final checkpoint at the end of training
|
| 381 |
+
if config.save_checkpoint:
|
| 382 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 383 |
+
workdir, jax_utils.unreplicate(state), config.max_steps,
|
| 384 |
+
keep=ckpt_keep, overwrite=True)
|
| 385 |
+
|
| 386 |
+
|
code/train/train_backtrack.py
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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_loc_threshold = float(getattr(config, "promote_loc_threshold", 0.70))
|
| 428 |
+
instance_mode = bool(getattr(config, "instance_dir", None))
|
| 429 |
+
promote_patience = ctx["promote_patience"]
|
| 430 |
+
min_stage_steps = ctx["min_stage_steps"]
|
| 431 |
+
ckpt_keep = ctx["ckpt_keep"]
|
| 432 |
+
stage_ckpt_dir = ctx["stage_ckpt_dir"]
|
| 433 |
+
|
| 434 |
+
# Controller state.
|
| 435 |
+
grad_acc = {} # stage t -> level-(t+2) val_acc at graduation
|
| 436 |
+
plateau_steps = int(getattr(config, "plateau_steps", 0))
|
| 437 |
+
plateau_delta = float(getattr(config, "plateau_delta", 0.005))
|
| 438 |
+
stage_best_acc = -1.0
|
| 439 |
+
stage_best_step = start_step
|
| 440 |
+
mode = "frontier" # "frontier" | "repair"
|
| 441 |
+
repair_stage = None # stage currently being repaired
|
| 442 |
+
repair_started_at = 0 # step the current repair stage began (cap)
|
| 443 |
+
repair_episode_start = 0 # step we first left the frontier (clock credit)
|
| 444 |
+
stage_started_at = start_step # step the current frontier stage began
|
| 445 |
+
last_repair_return_step = start_step # for min-frontier cooldown
|
| 446 |
+
step_counts = {t: 0 for t in range(1, K + 1)} # steps trained at each depth
|
| 447 |
+
repair_steps_total = 0 # for duty-cycle cap
|
| 448 |
+
print(f"[repair] knobs: margin={margin} max_repair_steps={max_repair_steps} "
|
| 449 |
+
f"min_frontier={min_frontier_steps} frontier_target={frontier_target_acc} "
|
| 450 |
+
f"max_repair_frac={max_repair_fraction} grad_decay={grad_decay} "
|
| 451 |
+
f"freeze_after={freeze_after_step} frontier_mix={frontier_mix}",
|
| 452 |
+
flush=True)
|
| 453 |
+
# When seeding from a mid-curriculum checkpoint (start stage > 1), the
|
| 454 |
+
# earlier stages graduated in the *source* run so we have no reference for
|
| 455 |
+
# them. Prefer an explicit SUDOKU_GRAD_ACC_SEED (true graduation refs);
|
| 456 |
+
# otherwise seed from the first eval (old behavior — can over-trigger).
|
| 457 |
+
seed_start_stage = int(getattr(config, "curriculum_start_stage", 1))
|
| 458 |
+
grad_acc_seeded = seed_start_stage <= 1
|
| 459 |
+
if (not grad_acc_seeded) and grad_acc_seed_raw.strip():
|
| 460 |
+
try:
|
| 461 |
+
vals = [float(x) for x in grad_acc_seed_raw.split(",") if x.strip()]
|
| 462 |
+
for s, v in enumerate(vals, start=1):
|
| 463 |
+
if s < seed_start_stage:
|
| 464 |
+
grad_acc[s] = v
|
| 465 |
+
if grad_acc:
|
| 466 |
+
grad_acc_seeded = True
|
| 467 |
+
print(f"[repair] seeded graduation refs from env: "
|
| 468 |
+
f"{dict((k, round(v, 3)) for k, v in grad_acc.items())}",
|
| 469 |
+
flush=True)
|
| 470 |
+
except ValueError:
|
| 471 |
+
print(f"[repair] WARNING: bad SUDOKU_GRAD_ACC_SEED="
|
| 472 |
+
f"{grad_acc_seed_raw!r}; falling back to first-eval seeding",
|
| 473 |
+
flush=True)
|
| 474 |
+
|
| 475 |
+
def compute_deficits(frontier_stage, per_depth):
|
| 476 |
+
"""{depth t: graduation_acc[t] - current_acc} over earlier graduated
|
| 477 |
+
depths whose current candidate-set accuracy is measured.
|
| 478 |
+
|
| 479 |
+
Keyed on reasoning depth, not difficulty level: depth t's accuracy is
|
| 480 |
+
how well wave snapshot t is predicted, which is exactly what stage t
|
| 481 |
+
taught. A drop there means that propagation block has been forgotten."""
|
| 482 |
+
d = {}
|
| 483 |
+
for t in range(1, frontier_stage):
|
| 484 |
+
if t in grad_acc and per_depth.get(t, -1.0) >= 0:
|
| 485 |
+
d[t] = grad_acc[t] - per_depth.get(t, -1.0)
|
| 486 |
+
return d
|
| 487 |
+
|
| 488 |
+
def effective_margin(per_depth, frontier_stage):
|
| 489 |
+
"""Widen the repair trigger while the frontier itself is still weak."""
|
| 490 |
+
m = margin
|
| 491 |
+
if frontier_target_acc > 0:
|
| 492 |
+
f_acc = per_depth.get(frontier_stage, -1.0)
|
| 493 |
+
if 0.0 <= f_acc < frontier_target_acc:
|
| 494 |
+
# Only repair clearer regressions until the frontier is good.
|
| 495 |
+
m = max(m, margin + (frontier_target_acc - f_acc))
|
| 496 |
+
return m
|
| 497 |
+
|
| 498 |
+
def most_deficient(frontier_stage, per_depth, use_margin=None):
|
| 499 |
+
"""Most-regressed depth whose drop exceeds the margin, else None."""
|
| 500 |
+
d = compute_deficits(frontier_stage, per_depth)
|
| 501 |
+
if not d:
|
| 502 |
+
return None
|
| 503 |
+
m = margin if use_margin is None else use_margin
|
| 504 |
+
t = max(d, key=d.get)
|
| 505 |
+
return t if d[t] > m else None
|
| 506 |
+
|
| 507 |
+
with metric_writers.ensure_flushes(writer):
|
| 508 |
+
for step in range(start_step, config.max_steps):
|
| 509 |
+
if step % 10000 == 0:
|
| 510 |
+
print("Step:", step, flush=True)
|
| 511 |
+
|
| 512 |
+
i = curriculum.stage
|
| 513 |
+
# Target depth: the frontier when training normally, else the depth
|
| 514 |
+
# we are repairing. Difficulty is never gated, so a repair differs
|
| 515 |
+
# from a frontier step only in the recurrence depth it trains at:
|
| 516 |
+
# replaying depth t re-supervises wave snapshots 1..t on the same
|
| 517 |
+
# full-corpus batch distribution.
|
| 518 |
+
t = repair_stage if mode == "repair" else i
|
| 519 |
+
batch = sampler.sample_all(config.minibatch_size)
|
| 520 |
+
state, metrics = _run_train_step(
|
| 521 |
+
p_frontier[t], state, batch, dropout_rngs)
|
| 522 |
+
step_counts[t] += 1
|
| 523 |
+
if mode == "repair":
|
| 524 |
+
repair_steps_total += 1
|
| 525 |
+
|
| 526 |
+
for h in hooks:
|
| 527 |
+
h(step)
|
| 528 |
+
|
| 529 |
+
if math.isnan(metrics["loss"][0]):
|
| 530 |
+
print("Loss became nan; stopping.", flush=True)
|
| 531 |
+
break
|
| 532 |
+
|
| 533 |
+
if step % config.eval_every_steps == 0:
|
| 534 |
+
eval_metrics = evaluater.get_eval_metrics(
|
| 535 |
+
state, eval_data_iter, p_eval_step, config)
|
| 536 |
+
per_level = eval_metrics.pop("per_level_acc")
|
| 537 |
+
# Depth accuracy drives the controller. Changed-cells-only, so
|
| 538 |
+
# a slot that merely copies its predecessor scores zero credit.
|
| 539 |
+
per_depth = eval_metrics.pop("per_slot_acc_changed", {})
|
| 540 |
+
per_depth_all = eval_metrics.pop("per_slot_acc", {})
|
| 541 |
+
per_stage_inset = eval_metrics.pop("per_stage_inset_acc", {})
|
| 542 |
+
if instance_mode:
|
| 543 |
+
# Repair if a stage's in-set rate falls behind its
|
| 544 |
+
# graduation value. The candidate head is off.
|
| 545 |
+
per_depth = per_stage_inset
|
| 546 |
+
elif not any(v >= 0 for v in per_depth.values()):
|
| 547 |
+
per_depth = per_depth_all
|
| 548 |
+
|
| 549 |
+
def _m(key):
|
| 550 |
+
v = eval_metrics.get(key, [])
|
| 551 |
+
return round(float(np.mean(v)), 4) if len(v) else -1.0
|
| 552 |
+
|
| 553 |
+
# Seed graduation refs for stages inherited from a checkpoint.
|
| 554 |
+
if not grad_acc_seeded:
|
| 555 |
+
for s in range(1, seed_start_stage):
|
| 556 |
+
acc_s = per_depth.get(s, -1.0)
|
| 557 |
+
if acc_s >= 0:
|
| 558 |
+
grad_acc[s] = float(acc_s)
|
| 559 |
+
grad_acc_seeded = True
|
| 560 |
+
print(f"[repair] step {step}: seeded graduation refs from "
|
| 561 |
+
f"resume: "
|
| 562 |
+
f"{dict((k, round(v, 3)) for k, v in grad_acc.items())}",
|
| 563 |
+
flush=True)
|
| 564 |
+
|
| 565 |
+
# Soft graduation refs: forgive chronic mild regression.
|
| 566 |
+
if grad_decay > 0 and grad_acc:
|
| 567 |
+
for s in list(grad_acc.keys()):
|
| 568 |
+
cur_s = per_depth.get(s, -1.0)
|
| 569 |
+
if cur_s >= 0 and cur_s < grad_acc[s]:
|
| 570 |
+
old = grad_acc[s]
|
| 571 |
+
grad_acc[s] = (
|
| 572 |
+
(1.0 - grad_decay) * grad_acc[s]
|
| 573 |
+
+ grad_decay * float(cur_s))
|
| 574 |
+
if step % (config.eval_every_steps * 5) == 0:
|
| 575 |
+
print(f"[repair] soft-grad depth {s}: "
|
| 576 |
+
f"{old:.3f}->{grad_acc[s]:.3f} "
|
| 577 |
+
f"(cur={cur_s:.3f})", flush=True)
|
| 578 |
+
|
| 579 |
+
frontier_acc = per_depth.get(curriculum.stage, -1.0)
|
| 580 |
+
eff_margin = effective_margin(per_depth, i)
|
| 581 |
+
deficits = compute_deficits(i, per_depth)
|
| 582 |
+
elapsed = max(1, step - start_step + 1)
|
| 583 |
+
repair_frac = repair_steps_total / float(elapsed)
|
| 584 |
+
bt_frozen = (
|
| 585 |
+
freeze_after_step > 0 and step >= freeze_after_step)
|
| 586 |
+
print(step, "stage", curriculum.stage,
|
| 587 |
+
"mode", mode,
|
| 588 |
+
"repair_stage", repair_stage,
|
| 589 |
+
"target_t", t,
|
| 590 |
+
"loss", round(float(metrics["loss"].mean()), 4),
|
| 591 |
+
"ce", round(float(metrics["ce_loss"].mean()), 4),
|
| 592 |
+
"aux_bce", round(float(metrics["aux_loss"].mean()), 4),
|
| 593 |
+
"| val_acc", _m("acc"), "loc_acc", _m("loc_acc"),
|
| 594 |
+
"val|loc", _m("val_given_loc_acc"),
|
| 595 |
+
"| frontier_acc", round(float(frontier_acc), 4),
|
| 596 |
+
"eff_margin", round(float(eff_margin), 4),
|
| 597 |
+
"| cand_bit_acc", _m("cand_bit_acc"),
|
| 598 |
+
"cand_set_acc", _m("cand_set_acc"),
|
| 599 |
+
"cand_set_chg", _m("cand_set_acc_changed"),
|
| 600 |
+
"| grad_acc", {k: round(v, 3) for k, v in grad_acc.items()},
|
| 601 |
+
"deficits", {k: round(v, 3) for k, v in deficits.items()},
|
| 602 |
+
"step_counts", dict(step_counts),
|
| 603 |
+
"repair_frac", round(repair_frac, 3),
|
| 604 |
+
"bt_frozen", bt_frozen,
|
| 605 |
+
flush=True)
|
| 606 |
+
|
| 607 |
+
with tf_summary_writer.as_default():
|
| 608 |
+
tf.summary.scalar("loss", metrics["loss"].mean(), step=step)
|
| 609 |
+
tf.summary.scalar("ce_loss", metrics["ce_loss"].mean(), step=step)
|
| 610 |
+
tf.summary.scalar("aux_bce_loss", metrics["aux_loss"].mean(), step=step)
|
| 611 |
+
tf.summary.scalar("curriculum_stage", curriculum.stage, step=step)
|
| 612 |
+
tf.summary.scalar("repair_mode", 1 if mode == "repair" else 0, step=step)
|
| 613 |
+
tf.summary.scalar("repair_stage", repair_stage or 0, step=step)
|
| 614 |
+
if frontier_acc >= 0:
|
| 615 |
+
tf.summary.scalar("frontier_depth_acc", frontier_acc, step=step)
|
| 616 |
+
tf.summary.scalar("eff_repair_margin", eff_margin, step=step)
|
| 617 |
+
for key in eval_metrics.keys():
|
| 618 |
+
tf.summary.scalar("eval_" + key,
|
| 619 |
+
np.array(eval_metrics[key]).mean(), step=step)
|
| 620 |
+
for lvl, v in per_level.items():
|
| 621 |
+
if v >= 0:
|
| 622 |
+
tf.summary.scalar(f"eval_acc_level_{lvl}", v, step=step)
|
| 623 |
+
for s, v in per_depth_all.items():
|
| 624 |
+
if v >= 0:
|
| 625 |
+
tf.summary.scalar(f"eval_cand_depth_{s}", v, step=step)
|
| 626 |
+
|
| 627 |
+
def _save_stage_ckpt(tag):
|
| 628 |
+
if config.save_checkpoint:
|
| 629 |
+
unrep = jax_utils.unreplicate(state)
|
| 630 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 631 |
+
workdir, unrep, step, keep=ckpt_keep, overwrite=True)
|
| 632 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 633 |
+
stage_ckpt_dir, unrep, step, keep=100,
|
| 634 |
+
overwrite=True, prefix=f"{tag}_")
|
| 635 |
+
|
| 636 |
+
# ---------------- Deficit-driven controller ----------------
|
| 637 |
+
# Freeze / duty-cycle: force frontier-only when budget exhausted.
|
| 638 |
+
duty_ok = (
|
| 639 |
+
max_repair_fraction <= 0
|
| 640 |
+
or repair_frac < max_repair_fraction)
|
| 641 |
+
if bt_frozen and mode == "repair":
|
| 642 |
+
print(f"[repair] step {step}: freeze_after="
|
| 643 |
+
f"{freeze_after_step}; leaving repair", flush=True)
|
| 644 |
+
mode = "frontier"
|
| 645 |
+
repair_stage = None
|
| 646 |
+
last_repair_return_step = step
|
| 647 |
+
if (not duty_ok) and mode == "repair":
|
| 648 |
+
print(f"[repair] step {step}: duty-cycle cap "
|
| 649 |
+
f"(repair_frac={repair_frac:.3f}>="
|
| 650 |
+
f"{max_repair_fraction}); return to frontier",
|
| 651 |
+
flush=True)
|
| 652 |
+
mode = "frontier"
|
| 653 |
+
repair_stage = None
|
| 654 |
+
last_repair_return_step = step
|
| 655 |
+
|
| 656 |
+
if mode == "frontier":
|
| 657 |
+
cooldown_ok = (
|
| 658 |
+
min_frontier_steps <= 0
|
| 659 |
+
or (step - last_repair_return_step) >= min_frontier_steps)
|
| 660 |
+
can_repair = (not bt_frozen) and duty_ok and cooldown_ok
|
| 661 |
+
worst = (most_deficient(i, per_depth, use_margin=eff_margin)
|
| 662 |
+
if can_repair else None)
|
| 663 |
+
if worst is not None:
|
| 664 |
+
mode = "repair"
|
| 665 |
+
repair_stage = worst
|
| 666 |
+
repair_started_at = step
|
| 667 |
+
repair_episode_start = step
|
| 668 |
+
print(f"[repair] step {step}: enter repair of depth "
|
| 669 |
+
f"{worst} (snapshot {worst} acc="
|
| 670 |
+
f"{per_depth.get(worst, -1.0):.3f} < grad "
|
| 671 |
+
f"{grad_acc.get(worst, -1.0):.3f} - "
|
| 672 |
+
f"eff_margin {eff_margin:.3f}; "
|
| 673 |
+
f"frontier_acc={frontier_acc:.3f})",
|
| 674 |
+
flush=True)
|
| 675 |
+
elif bt_frozen and most_deficient(
|
| 676 |
+
i, per_depth, use_margin=eff_margin) is not None:
|
| 677 |
+
if step % (config.eval_every_steps * 5) == 0:
|
| 678 |
+
print(f"[repair] step {step}: deficit present but "
|
| 679 |
+
f"BT frozen after {freeze_after_step}",
|
| 680 |
+
flush=True)
|
| 681 |
+
elif (not duty_ok) and most_deficient(
|
| 682 |
+
i, per_depth, use_margin=eff_margin) is not None:
|
| 683 |
+
if step % (config.eval_every_steps * 5) == 0:
|
| 684 |
+
print(f"[repair] step {step}: deficit present but "
|
| 685 |
+
f"duty-cycle cap "
|
| 686 |
+
f"(frac={repair_frac:.3f})",
|
| 687 |
+
flush=True)
|
| 688 |
+
elif (not cooldown_ok) and most_deficient(
|
| 689 |
+
i, per_depth, use_margin=eff_margin) is not None:
|
| 690 |
+
print(f"[repair] step {step}: deficit present but "
|
| 691 |
+
f"frontier cooldown "
|
| 692 |
+
f"({step - last_repair_return_step}/"
|
| 693 |
+
f"{min_frontier_steps}); staying on stage {i}",
|
| 694 |
+
flush=True)
|
| 695 |
+
elif curriculum.stage < curriculum.max_stage:
|
| 696 |
+
# Normal promotion (same rule as the standard loop).
|
| 697 |
+
steps_in_stage = step - stage_started_at
|
| 698 |
+
# A negative accuracy means "not measured this eval": it
|
| 699 |
+
# must not reset the plateau tracker nor satisfy the
|
| 700 |
+
# threshold/plateau rule. Only patience fires unmeasured.
|
| 701 |
+
measured = frontier_acc >= 0
|
| 702 |
+
if measured and frontier_acc > stage_best_acc + plateau_delta:
|
| 703 |
+
stage_best_acc = frontier_acc
|
| 704 |
+
stage_best_step = step
|
| 705 |
+
loc_now = _m("loc_acc")
|
| 706 |
+
loc_ready = (not instance_mode) or loc_now >= promote_loc_threshold
|
| 707 |
+
hit_threshold = (measured and loc_ready
|
| 708 |
+
and frontier_acc >= promote_threshold)
|
| 709 |
+
stalled = (measured and loc_ready and plateau_steps > 0
|
| 710 |
+
and (step - stage_best_step) >= plateau_steps)
|
| 711 |
+
patience_over = steps_in_stage >= promote_patience
|
| 712 |
+
if steps_in_stage >= min_stage_steps and (
|
| 713 |
+
hit_threshold or stalled or patience_over):
|
| 714 |
+
reason = ("threshold" if hit_threshold
|
| 715 |
+
else "plateau" if stalled else "patience")
|
| 716 |
+
# Record this stage's graduation accuracy BEFORE moving on.
|
| 717 |
+
grad_acc[curriculum.stage] = float(frontier_acc)
|
| 718 |
+
curriculum.stage += 1
|
| 719 |
+
stage_started_at = step
|
| 720 |
+
stage_best_acc = -1.0
|
| 721 |
+
stage_best_step = step
|
| 722 |
+
print(f"[curriculum] step {step}: promote to stage "
|
| 723 |
+
f"{curriculum.stage} ({reason}; graduated "
|
| 724 |
+
f"depth {curriculum.stage - 1} cand-set "
|
| 725 |
+
f"acc={frontier_acc:.3f})", flush=True)
|
| 726 |
+
_save_stage_ckpt(f"stage{curriculum.stage}")
|
| 727 |
+
elif (frontier_target_acc > 0
|
| 728 |
+
and 0.0 <= frontier_acc < frontier_target_acc
|
| 729 |
+
and step % (config.eval_every_steps * 5) == 0):
|
| 730 |
+
print(f"[frontier] step {step}: depth {i} "
|
| 731 |
+
f"acc={frontier_acc:.3f} "
|
| 732 |
+
f"< target {frontier_target_acc:.3f}; "
|
| 733 |
+
f"keeping frontier priority",
|
| 734 |
+
flush=True)
|
| 735 |
+
else: # mode == "repair"
|
| 736 |
+
r = repair_stage
|
| 737 |
+
cur = per_depth.get(r, -1.0)
|
| 738 |
+
ref = grad_acc.get(r, -1.0)
|
| 739 |
+
recovered = cur >= (ref - margin)
|
| 740 |
+
capped = (step - repair_started_at) >= max_repair_steps
|
| 741 |
+
if recovered or capped:
|
| 742 |
+
why = "recovered" if recovered else "cap"
|
| 743 |
+
print(f"[repair] step {step}: depth {r} done ({why}; "
|
| 744 |
+
f"snapshot acc={cur:.3f} vs grad {ref:.3f})",
|
| 745 |
+
flush=True)
|
| 746 |
+
_save_stage_ckpt(f"repair{r}")
|
| 747 |
+
# Re-scan: chain to the next most-deficient stage, or
|
| 748 |
+
# return to the frontier and credit repair time back to
|
| 749 |
+
# the frontier stage's patience clock.
|
| 750 |
+
nxt = None
|
| 751 |
+
if (not bt_frozen) and duty_ok:
|
| 752 |
+
nxt = most_deficient(
|
| 753 |
+
i, per_depth, use_margin=eff_margin)
|
| 754 |
+
if nxt is not None:
|
| 755 |
+
repair_stage = nxt
|
| 756 |
+
repair_started_at = step
|
| 757 |
+
print(f"[repair] step {step}: chain to stage {nxt}",
|
| 758 |
+
flush=True)
|
| 759 |
+
else:
|
| 760 |
+
mode = "frontier"
|
| 761 |
+
repair_stage = None
|
| 762 |
+
last_repair_return_step = step
|
| 763 |
+
stage_started_at += (step - repair_episode_start)
|
| 764 |
+
|
| 765 |
+
if config.save_checkpoint and step > 0 and step % config.save_every_steps == 0:
|
| 766 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 767 |
+
workdir, jax_utils.unreplicate(state), step,
|
| 768 |
+
keep=ckpt_keep, overwrite=True)
|
| 769 |
+
|
| 770 |
+
if config.save_checkpoint:
|
| 771 |
+
checkpoints.save_checkpoint_multiprocess(
|
| 772 |
+
workdir, jax_utils.unreplicate(state), config.max_steps,
|
| 773 |
+
keep=ckpt_keep, overwrite=True)
|
code/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/verify_superposition_instances.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Statistics for superposition-instance generation across all stages.
|
| 2 |
+
|
| 3 |
+
Reports, per stage: how many instances are produced per puzzle, how much of each
|
| 4 |
+
candidate set the instances cover, how many proposals get ruled out by the
|
| 5 |
+
dependencies, and how the dependency load changes as the grid resolves.
|
| 6 |
+
"""
|
| 7 |
+
import argparse
|
| 8 |
+
import collections
|
| 9 |
+
import multiprocessing as mp
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
import superposition_instances as SI
|
| 14 |
+
|
| 15 |
+
_MASKS = None
|
| 16 |
+
_ARGS = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _init(path, args):
|
| 20 |
+
global _MASKS, _ARGS
|
| 21 |
+
_MASKS = np.load(path, mmap_mode="r")
|
| 22 |
+
_ARGS = args
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _one(idx):
|
| 26 |
+
out = []
|
| 27 |
+
n_stages = _MASKS.shape[1]
|
| 28 |
+
for s in range(n_stages):
|
| 29 |
+
mask = np.array(_MASKS[idx, s]).astype(np.uint16)
|
| 30 |
+
r = SI.instances_for_stage(
|
| 31 |
+
mask, max_confine=_ARGS.max_confine,
|
| 32 |
+
max_instances=_ARGS.max_instances,
|
| 33 |
+
max_attempts=_ARGS.max_attempts, seed=idx * 100 + s,
|
| 34 |
+
max_repair=_ARGS.max_repair)
|
| 35 |
+
all_deps = SI.dependencies_from_mask(mask, max_confine=9)
|
| 36 |
+
r["n_deps_all"] = len(all_deps)
|
| 37 |
+
r.pop("instances")
|
| 38 |
+
r["stage"] = s
|
| 39 |
+
out.append(r)
|
| 40 |
+
return out
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def main():
|
| 44 |
+
ap = argparse.ArgumentParser()
|
| 45 |
+
ap.add_argument("--masks", default="datasets_multicandidate_s12/test_cand_masks.npy")
|
| 46 |
+
ap.add_argument("--limit", type=int, default=200)
|
| 47 |
+
ap.add_argument("--max-confine", type=int, default=3)
|
| 48 |
+
ap.add_argument("--max-instances", type=int, default=32)
|
| 49 |
+
ap.add_argument("--max-attempts", type=int, default=200)
|
| 50 |
+
ap.add_argument("--max-repair", type=int, default=120)
|
| 51 |
+
ap.add_argument("--workers", type=int, default=32)
|
| 52 |
+
args = ap.parse_args()
|
| 53 |
+
|
| 54 |
+
masks = np.load(args.masks, mmap_mode="r")
|
| 55 |
+
n = min(args.limit, len(masks))
|
| 56 |
+
print(f"masks {masks.shape} from {args.masks}")
|
| 57 |
+
print(f"puzzles {n}, confinement threshold |S| <= {args.max_confine}, "
|
| 58 |
+
f"instance cap {args.max_instances}, attempt cap {args.max_attempts}")
|
| 59 |
+
|
| 60 |
+
with mp.Pool(args.workers, initializer=_init,
|
| 61 |
+
initargs=(args.masks, args)) as pool:
|
| 62 |
+
results = pool.map(_one, range(n), chunksize=4)
|
| 63 |
+
|
| 64 |
+
by_stage = collections.defaultdict(list)
|
| 65 |
+
for rows in results:
|
| 66 |
+
for r in rows:
|
| 67 |
+
by_stage[r["stage"]].append(r)
|
| 68 |
+
|
| 69 |
+
print()
|
| 70 |
+
print("=" * 112)
|
| 71 |
+
print("PER-STAGE INSTANCE GENERATION")
|
| 72 |
+
print("=" * 112)
|
| 73 |
+
print(f"{'stage':>5} {'empty':>6} {'width':>6} {'deps all':>9} "
|
| 74 |
+
f"{'deps enf':>9} {'kept':>6} {'ruled out':>10} {'reject %':>9} "
|
| 75 |
+
f"{'coverage':>9} {'capped %':>9} {'dup/inst':>9}")
|
| 76 |
+
print("-" * 112)
|
| 77 |
+
for s in sorted(by_stage):
|
| 78 |
+
rs = by_stage[s]
|
| 79 |
+
g = lambda k: np.mean([r[k] for r in rs])
|
| 80 |
+
rej = np.sum([r["n_rejected"] for r in rs])
|
| 81 |
+
att = np.sum([r["n_attempts"] for r in rs])
|
| 82 |
+
print(f"{s:>5} {g('n_empty'):>6.1f} {g('mean_width'):>6.2f} "
|
| 83 |
+
f"{g('n_deps_all'):>9.1f} {g('n_deps'):>9.1f} "
|
| 84 |
+
f"{g('n_instances'):>6.1f} {g('n_rejected'):>10.1f} "
|
| 85 |
+
f"{100*rej/max(att,1):>8.1f}% {100*g('coverage'):>8.1f}% "
|
| 86 |
+
f"{100*np.mean([r['hit_cap'] for r in rs]):>8.1f}% "
|
| 87 |
+
f"{g('mean_dups'):>9.2f}")
|
| 88 |
+
|
| 89 |
+
tot_inst = np.sum([r["n_instances"] for rs in by_stage.values() for r in rs])
|
| 90 |
+
tot_rej = np.sum([r["n_rejected"] for rs in by_stage.values() for r in rs])
|
| 91 |
+
print()
|
| 92 |
+
print(f"per puzzle across all {len(by_stage)} stages: "
|
| 93 |
+
f"{tot_inst/n:.1f} instances kept, {tot_rej/n:.1f} ruled out")
|
| 94 |
+
print(f"dataset multiplier vs one row per puzzle: {tot_inst/n:.1f}x")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
main()
|