File size: 10,853 Bytes
2c7b2f9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | # Usage guide (English)
This package is **DuoTactic** β a Lean 4 proof-search harness β together with the 39.58 M-parameter
tactic model (`leanoar-schemec-40m`) that it can use as an optional candidate generator.
* To **run the harness** (sections 3 onward) you need Lean `4.35.0-rc2` and a mathlib4 checkout with a
prebuilt `.lake` cache (~10 GB). This is what produces miniF2F `test` 32.8 % / `valid` 34.8 %, at
45 s per problem, with every accepted proof re-compiled by the kernel.
* To **load or sample the model** (sections 0β2) you need no Lean. Note that `SchemeC` is **not** a
standard `transformers` model: it is defined in `code/leanoar/model.py`, its weights are plain
`lit_model.pth` / `model.safetensors` files, and it emits a *single Lean tactic* per call. There is
no `AutoModelForCausalLM.from_pretrained`. Everything below is copy-paste runnable from the package
root.
```
duotactic/
βββ code/eval_minif2f.py DuoTactic itself: the end-to-end search harness
βββ code/leanoar/ search + batched verifier + the 40M model
βββ checkpoints/e6/ model weights (highest dev top-1)
βββ checkpoints/stage3-e3/ model weights, end-to-end best variant (same pass@1, 16% faster)
βββ data/dev/ dev protocol arrays (1024 rows)
βββ data/minif2f/ miniF2F valid/test records + .lean files
βββ data/whitelist/ seed for the search's first-token whitelist
βββ examples/dev_sample.jsonl 16 real dev states + the real mathlib tactic
βββ config.json metrics.json tokenizer_v1.json special_tokens_v1.json SHA256SUMS
βββ README.md README.zh.md docs/USAGE.md docs/USAGE.zh.md
```
## 0. Install and verify
```bash
pip install -r code/requirements.txt # torch>=2.4, tokenizers>=0.19, numpy>=1.26, safetensors>=0.4
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
python code/verify_package.py # full check, 1024 dev rows (β1 min on GPU)
python code/verify_package.py --quick # 128 rows, CPU-friendly
```
`verify_package.py` prints four sections and ends with `PASS` / `FAIL`:
1. **parameter count** β rebuilds `SchemeC` from `config.json`, must be 39,582,595 for both variants;
2. **dev protocol** β recomputes top-1/top-5/CE on the packaged 1024 rows and compares with
`metrics.json` (tolerance 5e-4). This is the check that makes the published numbers auditable;
3. **inference wiring** β runs the packaged example state through the sampler (it also shows whether
the model's preferred first token matches mathlib's; a miss is normal and does not fail the check);
4. **checksums** β every file against `SHA256SUMS`.
Exit code is 0 only if 1, 2 and 4 pass. Use `--skip-sums` if you only have the weights.
## 1. Standalone inference (no Lean)
```bash
python code/generate.py # packaged dev example #0
python code/generate.py --example 3 --k 8 # another real dev state
python code/generate.py --state-file state.txt # your own state (format below)
python code/generate.py --ckpt checkpoints/stage3-e3 # the other variant
```
A **state** is the text Lean prints for a goal, i.e. hypotheses then `β’ goal`:
```
n m : β
h : n < m
β’ n β€ m
```
You can obtain one from Lean with `trace_state` inside a proof, or from an `unsolved goals` error
message. In your own code:
```python
import json, os, sys, torch
sys.path.insert(0, 'code')
from common import load_model, load_tokenizer, encode_state, specials, whitelist
from generate import propose
net, cfg, device = load_model('checkpoints/e6') # or 'checkpoints/stage3-e3'
tok, wl = load_tokenizer(), whitelist()
for tactic, avg_logprob in propose(net, tok, wl, state, k=16, device=device):
... # send to Lean for verification
```
Four rules that matter:
* **Restrict the first token** to `first_token_whitelist.json` (64 ids, 97.6 % of training first
tokens). `propose()` already does this. Without it, most candidates start with meaningless tokens
and the type-check rate collapses.
* **One candidate is one tactic.** The model separates two intended steps by a double space and may
end a truncated tactic with a dangling connective; `propose()` cuts both.
* **Truncation**: states longer than `ctx β 4 β max_new_tokens` tokens keep the head 60 % and the
tail 40 % (the `β’ goal` line is last). Never feed more than `context_length` tokens total.
* **Only the Lean kernel decides correctness.** Ask for many candidates and verify them; do not trust
any single output. Whole-tactic exact match is 0/200, and a "valid" (type-checking) candidate is
still not a proof.
## 2. Reproduce the dev metric
```bash
python code/eval_dev.py --all --compare # both variants, 1024 rows, compare with metrics.json
python code/eval_dev.py --ckpt checkpoints/e6 --rows 256
```
Expected (tolerance 5e-4):
| variant | top-1 | top-5 | CE |
|---|---|---|---|
| `checkpoints/e6` | 0.3076 | 0.7090 | 2.5494 |
| `checkpoints/stage3-e3` | 0.2891 | 0.7246 | 2.4971 |
The protocol: take the first 1024 rows of `data/dev/dev_ids.npy` whose second token is `<|FORWARD|>`,
read the logits at `mask_start β 1` and score the token at `mask_start`. **Do not compare these with
the numbers a trainer prints** β those come from a length-bucketed reshuffle of the dev split.
## 3. End-to-end proof search on miniF2F (needs Lean + mathlib)
### 3.1 Prerequisites
* A **Lean 4.35.0-rc2** toolchain (e.g. `elan toolchain install leanprover/lean4:v4.35.0-rc2`).
Note: this is a *release candidate*; newer Lean versions will re-elaborate mathlib differently.
* A **mathlib4 checkout with a prebuilt `.lake` cache** (this project used the Sept 2026 mathlib
snapshot; the exact commit is not pinned in the release, so state text may drift slightly on other
revisions). Build the cache with `lake exe cache get` inside the checkout β a full local build takes
hours, the olean cache takes minutes and ~7 GB.
* β₯ 16 GB RAM if you use `--lean-parallel 6` (each `lean --json` process loads mathlib; ~0.4 s per
candidate, 6 in parallel measured at 0.034 s/candidate).
```bash
export LEANOAR_LEAN_BIN=/path/to/lean-4.35.0-rc2-linux/bin/lean
export LEANOAR_MATHLIB=/path/to/mathlib4
$LEANOAR_LEAN_BIN --version # sanity check
```
### 3.2 The run that produced the published number
```bash
python code/eval_minif2f.py --split valid --n 89 --start 0 \
--k 16 --beam 4 --depth 6 --budget 45 --round-budget 30 \
--force --force-set v2 --force-depth 99 --lean-parallel 6 \
--ckpt checkpoints/stage3-e3 --tag mine
```
Expected: **30/89 = 33.7 %**, β52 min on an RTX 4060 Laptop (the `e6` variant gives the same 30
problems in β60 min). The full `valid` split was run end-to-end with this exact configuration:
**85/244 = 34.8 %** in β2.3 h (segments 33.7 % / 40.4 % / 28.8 %; 78 of the 85 solutions are one
tactic). Longer runs are easier as `--lean-parallel` rises: the fixed cost of every `lean --json`
call is β6.6 s of `import Mathlib` (vs β0.03 s per candidate), so parallelism and import trimming
are where the wall clock goes.
What the flags do β this is where the score comes from:
| Flag | Meaning |
|---|---|
| `--k 16` | candidates per state (top-16 whitelisted first tokens, greedy continuation) |
| `--beam 4` | how many open states are expanded per round |
| `--depth 6` | max proof length in tactics (measured: 25 of 30 solutions are depth 1) |
| `--budget 45` | wall-clock seconds per problem; `--round-budget 30` caps a single round |
| `--force --force-set v2` | inject the exported automation table (`rfl`, `ring`, `simp_all`, `omega`, β¦, `simp_all ; ring`, `gcongr`) at every depth. **This is the single biggest lever**: with it the first-89 result is 30/89, without it the whole 244 gives 8/244. |
| `--lean-parallel 6` | verify 6 chunks of candidates concurrently (pure engineering: 2.07Γ wall clock) |
| `--narrow-imports` | per problem, replace the full Mathlib import by `#min_imports` + `Mathlib.Tactic` (measured 3.41 s β 1.74 s per `lean --json` call). A candidate that fails with "unknown identifier/tactic" is automatically re-checked with the full import, so narrowing cannot lose a proof (verified: 85/85 accepted valid proofs still compile) |
| `--no-model` | ablation switch: verify only the injected table, no model proposals. On valid problems 0-9 this reproduces the model's result exactly (2/10, same problems, same proof terms) in half the wall clock |
Before quoting any pass rate, read **βWhere this score comes fromβ** in `README.md`: these are
*system* numbers (injected automation table + search + kernel verification), and the shipped model
supplies 0 of the accepted proofs.
### 3.3 Reading and resuming results
Results are appended to `reports/search_eval_valid_mine.jsonl` (one JSON object per problem:
`solved`, `depth`, `seconds`, `verified`, `proof`, `confirmed`, `reason`) and every run prints
cumulative progress with an ETA. Because the file is append-only, an interrupted run resumes with
`--start <rows already written>` β or simply re-run the same command.
### 3.4 Troubleshooting
* `initial-state-failed` for a problem: its statement does not even compile against your mathlib
revision. Two miniF2F problems fail on mathlib master for everyone
(`mathd_numbertheory_711`, `imosl_2007_algebra_p6`).
* `No such file or directory: .../bin/lean`: set `LEANOAR_LEAN_BIN`.
* Lean errors about missing oleans / `unknown module prefix 'Mathlib'`: set `LEANOAR_MATHLIB` to a
checkout with a **built** `.lake/build/lib/lean` (and `.lake/packages/*`); the verifier assembles
`LEAN_PATH` from those paths.
* Very low `verified` counts: check that the first-token whitelist is being applied and that the
tokenizer is `tokenizer_v1.json`.
* A state longer than the prompt budget used to overflow the context by one token in old builds; the
shipped `search.py` budgets `ctx β 4 β max_new_tokens` and hard-stops at `ctx`. If you edit the
sampler, keep that invariant.
## 4. Extending
* Swap in your own weights: put `lit_model.pth` (+ `model.safetensors`) in a directory and pass
`--ckpt <dir>` to `generate.py` / `eval_dev.py` / `eval_minif2f.py`. `config.json` must match the
architecture; the vocabulary and context length are fixed.
* Add automation tactics to `FORCE_SETS` in `code/eval_minif2f.py` β measured gains in this project
came almost exclusively from that table and from the search/verifier side, not from the model.
* The value head is shipped but unusable (AUC 0.5201, below the policy's own confidence). Do not
build a reranker on it.
* `code/leanoar/data.py` is included because the search code imports `load_specials` from it; the
training pipeline itself is not part of this package.
|