Download docs/USAGE.md from Duoia/duotactic: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/Duoia/duotactic/resolve/main/docs/USAGE.md
- Command line
-
hf download hf://Duoia/duotactic/docs/USAGE.md
-
curl -L -o USAGE.md https://huggingface.co/Duoia/duotactic/resolve/main/docs/USAGE.md
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-rc2and a mathlib4 checkout with a prebuilt.lakecache (~10 GB). This is what produces miniF2Ftest32.8 % /valid34.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
SchemeCis not a standardtransformersmodel: it is defined incode/leanoar/model.py, its weights are plainlit_model.pth/model.safetensorsfiles, and it emits a single Lean tactic per call. There is noAutoModelForCausalLM.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
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:
- parameter count β rebuilds
SchemeCfromconfig.json, must be 39,582,595 for both variants; - 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; - 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);
- 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)
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:
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_tokenstokens keep the head 60 % and the tail 40 % (theβ’ goalline is last). Never feed more thancontext_lengthtokens 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
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
.lakecache (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 withlake exe cache getinside 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(eachlean --jsonprocess loads mathlib; ~0.4 s per candidate, 6 in parallel measured at 0.034 s/candidate).
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
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-failedfor 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: setLEANOAR_LEAN_BIN.- Lean errors about missing oleans /
unknown module prefix 'Mathlib': setLEANOAR_MATHLIBto a checkout with a built.lake/build/lib/lean(and.lake/packages/*); the verifier assemblesLEAN_PATHfrom those paths. - Very low
verifiedcounts: check that the first-token whitelist is being applied and that the tokenizer istokenizer_v1.json. - A state longer than the prompt budget used to overflow the context by one token in old builds; the
shipped
search.pybudgetsctx β 4 β max_new_tokensand hard-stops atctx. 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>togenerate.py/eval_dev.py/eval_minif2f.py.config.jsonmust match the architecture; the vocabulary and context length are fixed. - Add automation tactics to
FORCE_SETSincode/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.pyis included because the search code importsload_specialsfrom it; the training pipeline itself is not part of this package.