# Open Lean-prover teachers for distillation data (lane T2, 2026-10-19) Goal: verified proofs from stronger open provers as one more SFT source for the Apache-2.0 research model (base: Goedel-Prover-V2-8B). Nothing here touched a GPU. All sizes/licenses are from the HF hub API and model cards read on 2026-10-19; every miniF2F number is the AUTHORS' number under their own verifier, token cap and sampling, so none is comparable to what we measure until reproduced in our harness (plan section 1). ## 1. Survey | model | license (hub) | bf16 size | arch / Lean | reported miniF2F-test | verdict | |---|---|---|---|---|---| | **Goedel-LM/Goedel-Prover-V2-32B** | Apache-2.0 | 65.5 GB, 14 shards | Qwen3 dense, 64 L, 8 KV heads, ctx 40,960; Lean 4 + Mathlib, same stack as the 8B | 88.0% pass@32 (90.4% with self-correction); PutnamBench 43 @32 (57 with self-corr., 86 @192) | **best fitting teacher; downloaded** | | deepseek-ai/DeepSeek-Prover-V2-7B | repo `LICENSE` is MIT (code), model card says **DeepSeek Model License** (use-based restrictions, see 1.1) | 13.8 GB | Llama, 30 L, **32 KV heads (MHA)**, YaRN x16 to 65,536 (card: 32K) | not on the card (7B row is in the paper, arXiv 2504.21801; not re-read here, so unverified); 671B: 88.9% | downloaded; **training use blocked on license (1.1)** | | AI-MO/Kimina-Prover-72B | MIT | 145.4 GB | Qwen2, 80 L, 8 KV heads, ctx 131k | 84.0% pass@32 | does not fit bf16; no open AWQ/GPTQ safetensors (only GGUF); weaker than Goedel-32B; skip | | AI-MO/Kimina-Prover-Distill-8B (= `Kimina-Prover-8B`, same sha) | Apache-2.0 | 16.4 GB | Qwen3-8B | 77.86% pass@32 | already on disk (docs/KIMINA_BASE.md); same size and weaker than our base, not a teacher | | Kimina-Prover-Distill-1.7B / 0.6B, Preview-Distill-7B / 1.5B | Apache-2.0 | 4.1 / 1.5 / 15.2 / 3.6 GB | Qwen3 / Qwen2.5-Math | below the 8B | too weak to teach an 8B | | stepfun-ai/StepFun-Prover-Preview-32B / 7B | Apache-2.0 | 65.5 / 15.2 GB | Qwen2 (R1-distill base) | 70.0% pass@1 (32B) | multi-turn REPL loop (``/``), not whole-proof; incompatible with `lean_gen`; skip | | ByteDance-Seed/BFS-Prover-V2-7B / 32B | Apache-2.0 | 15.2 / 131.1 GB | Qwen2.5 | n/a here | next-tactic model for tree search, no whole-proof output; skip | | ByteDance-Seed/Seed-Prover | not open (hub 401; paper/API only) | - | - | - | unavailable | | mistralai/Leanstral-1.5-119B-A6B (and -2603) | Apache-2.0 | 120.9 GB | Mistral Small 4 MoE (119B total, ~6B active), agentic, ctx 256k | not read | does not fit bf16 on 92 GB; only NVFP4/GGUF/MLX community quants (Blackwell/Apple formats, untested on Ampere); agentic tool protocol, not whole-proof; separate lane | | deepseek-ai/DeepSeek-Prover-V2-671B | DeepSeek Model License | 688.6 GB | - | 88.9% | out of reach | | Goedel-LM/Goedel-Prover-SFT | MIT | 13.8 GB | DeepSeek-Prover-V1.5 base | ~57% | weaker than our base | Quantised open variants found by hub search (names containing awq/gptq/fp8/bnb/nvfp4): Goedel-V2-32B: only `mlx-community/...-8bit` (Apple); DeepSeek-Prover-V2-7B: `athanor-ai/...-GPTQ-4bit`, `unsloth/...-bnb-4bit`, `NVFP4/...-FP4`; Kimina-72B and Kimina-8B: none. None is needed: the two models we use fit in bf16 (section 2). Third-party quantised copies are not trusted as teachers anyway (unknown calibration, no license chain). ### 1.1 License finding (read before any DeepSeek output enters training) The DeepSeek Model License (LICENSE-MODEL, the one the Prover-V2 card links) defines "Derivatives of the Model" to include "any other model which is created ... by transfer of patterns of the ... output of the Model ... including distillation methods ... or methods based on the generation of synthetic data by the Model for training the other model". Section 4 requires every derivative to carry the Attachment-A use-based restrictions as an enforceable term. An SFT model trained on DeepSeek-Prover-V2 proofs is therefore a Derivative that cannot be released under plain Apache-2.0. The hub repo's `LICENSE` file is MIT, which contradicts the card; the card + LICENSE-MODEL is the safer reading. **Decision for the owner**: either (a) do not train on DeepSeek-Prover-V2 outputs (default here), or (b) accept Attachment-A on the released weights. Using it for evaluation or as a baseline is unaffected. Goedel-V2-32B (Apache-2.0) and Kimina (MIT / Apache-2.0) carry no such clause. Apache-2.0 / MIT model cards do not mention an output-use clause; we record the hub license string and card revision per teacher anyway. ## 2. Feasibility on 4x A10G (22.5 GiB usable each, 90 GiB total, TP=4, PCIe + host-SHM NCCL) Model: per-GPU weights = bytes/4; KV bytes/token (global) = layers x 2 x kv_heads x head_dim x 2; each GPU stores 1/4 of the heads. Per decode step each GPU reads its weight shard plus the live KV; step time = bytes / (eff x 600 GB/s). `eff` is calibrated on OUR 8B measurement: 256 seqs at cap 16k = 1,363 tok/s (docs/P2.md, P2-B), mean completion 4,996 tokens, mean live context about 5,000 gives eff = 0.46 (this folds in the NCCL cost). The model reproduces the 8B at 84 seqs as 1,171 tok/s (measured 1,043 end-to-end / 1,015 in the smoke). Overhead reserved per GPU: 2.0 GiB (CUDA context, activations, graphs, NCCL). These are ESTIMATES; the section 6 smoke replaces them. | | Goedel-8B (ours, measured) | Goedel-V2-32B bf16 | same, util 0.94 | same, fp8 KV (untested on A10G) | DeepSeek-Prover-V2-7B bf16 | same, fp8 KV | |---|---|---|---|---|---|---| | weights / GPU | 3.8 GiB | **15.3 GiB** | 15.3 | 15.3 | 3.2 GiB | 3.2 | | KV bytes / token (global) | 144 KiB | 256 KiB | 256 | 128 | **480 KiB (MHA)** | 240 | | KV room / GPU at util 0.90 | 14.4 GiB | **3.0 GiB** | 3.9 (0.94) | 3.0 | 15.0 GiB | 15.0 | | KV capacity (tokens) | 420k | **49k** | 64k | 98k | 131k | 263k | | sequences at 16k / 32k (worst case) | 25 / 13 | **3.0 / 1.5** | 3.9 / 1.9 | 6 / 3 | 8 / 4 | 16 / 8 | | sequences at mean live ctx 5k | 84 | **9** | 12 | 19 | 26 | 52 | | est. tok/s | 1,000-1,360 | **~130** | ~160 | ~270 | ~365 | ~730 | | `max_model_len` it can start with | 40,960 | 40,960 needs 2.5 GiB/GPU for one sequence: starts at 0.90, no headroom; use <= 33,000 | | | 32,768 | | Reading: bf16 weights of the 32B fit (61 GiB of 90) but leave only ~3-4 GiB per GPU for KV, so the engine runs ~9-12 concurrent sequences, is KV-capacity-bound and runs at roughly **130-160 tok/s, 7-8x slower than the 8B**. It is the only open model above our base that fits at all. The 7B teacher is not fast either: it is MHA (32 KV heads, 3.3x the KV per token of Qwen3-8B), so its KV room, not its weights, caps it at ~365 tok/s. Alternatives considered: TP=2 x 2 replicas for the 7B gives the same total capacity and doubles the engine count (no throughput gain, more load time); a 4-bit 32B (~18 GB) would free ~10 GiB of KV per GPU and ~4x the concurrency, but no trustworthy open GPTQ/AWQ of Goedel-V2-32B exists and making one needs a calibration run and 65 GB scratch we do not have headroom for: **not pursued**. The cheapest lever is `--kv-cache-dtype fp8` in vLLM (2x capacity); on Ampere it is storage-only fp8 with a slower attention path, so measure it in the smoke before trusting the ~270 tok/s. Prefix caching (already on) helps nothing here: prompts are short and unique. ## 3. What was downloaded All via `scripts/teachers/download_teacher.py` (disk floor 60 GB enforced against hub-reported bytes BEFORE fetching; 2 workers, `nice -n 10 ionice -c3`, hostguard cap 2 cores / 8 GB, `HF_HUB_DISABLE_XET=1`), into `HF_HOME=/apps/development/rlhf/hf_cache`. | repo | revision | path | size | check | |---|---|---|---|---| | deepseek-ai/DeepSeek-Prover-V2-7B | `a8d9e14432b2e8dd9df2a4d4e70f1ba9bc8d9b7b` | `hub/models--deepseek-ai--DeepSeek-Prover-V2-7B/snapshots/a8d9e144...` | 13.8 GB, 2 bf16 shards | `check_snapshot.py`: 273/273 tensors, shapes match, shards complete, 273 x BF16 | | Goedel-LM/Goedel-Prover-V2-32B | `851bf85d329b0f819e1a44db30e05d16e07d15c0` | `hub/models--Goedel-LM--Goedel-Prover-V2-32B/snapshots/851bf85d...` | 65.5 GB | `check_snapshot.py`: 14/14 shards complete, 707/707 tensors present exactly once, every shape matches, 707 x BF16, all 25 files byte-size equal to the hub's; **warning: the upstream `model.safetensors.index.json` assigns 657 of 707 tensors to the wrong shard** (3.2) | Disk: 148 GB free before the 32B (166 GB at the start of the lane), **74 GB free after** both (floor 60 GB; the disk is shared and other writers took ~10-15 GB during the 90 minutes, so re-check `df -h /apps` before any further download). Both downloads resumed cleanly after one `ReadTimeout`; no `.incomplete` blobs remain. ### 3.1 Verification tools * `scripts/teachers/check_snapshot.py SNAPSHOT [--json]` (new, stdlib only, header-only, tested in `tests/test_teacher_check_snapshot.py`): config architecture (Llama / Qwen2 / Qwen3 dense), index key set equals the key set the config implies, every shard exists, header extents equal the file size (truncation), tensor-in-index-shard, every shape, dtypes. It reports `rope_scaling` / `sliding_window` / `tie_word_embeddings` rather than refusing. * `scripts/p2/check_qwen3_snapshot.py` for the Qwen3-family 32B as well (it additionally proves the key set maps into `align`'s loader). `align` does not need to LOAD the 32B (teacher use is vLLM generation only); the check is for completeness. ### 3.2 Findings on the 32B snapshot * **Upstream index defect.** All 707 tensors exist exactly once across the 14 shards and every shard header matches its file size, but `model.safetensors.index.json` (byte-identical to the hub's at this revision) names the wrong shard for 657 of them (e.g. `model.embed_tokens.weight` is in shard 1, the index says shard 3). vLLM and transformers read every shard listed in `weight_map` and are unaffected; `scripts/p2/check_qwen3_snapshot.py` reports NOT COMPATIBLE because `align`'s loader keys on the index. Consequence: use the 32B ONLY through vLLM (as planned); if `align` ever needs to load it, regenerate the index from the shard headers (a 20-line script, no weight I/O) rather than patching the snapshot in place. `check_snapshot.py --strict-index` turns the mismatch into a failure. * Config: Qwen3ForCausalLM, 64 layers, hidden 5120, ffn 25600, 64 Q / 8 KV heads, head_dim 128, vocab 151,936, rope_theta 1e6, no rope scaling, max_position_embeddings 40,960, bf16, untied head. * Tokenizer: `tokenizer.json`, `vocab.json`, `merges.txt`, `added_tokens.json`, `special_tokens_map.json`, `tokenizer_config.json` (so the chat template) are **byte-identical to Goedel-8B's**: token ids and the prompt render are unchanged. * `generation_config.json` differs: 32B has only `eos_token_id 151645`, no sampling defaults; the 8B has temperature 0.6, top_k 20, top_p 0.95 and EOS `[151645, 151643]`. `lean_gen` passes temperature/top_p explicitly, but not top_k: do not let vLLM's `generation_config="auto"` supply different defaults per model (pass `--generation-config vllm`, or set top_k explicitly) so 8B and 32B samples come from the same sampler. ## 4. Prompt formats (record of what each teacher expects) | | Goedel-Prover-V2-32B | DeepSeek-Prover-V2-7B | |---|---|---| | chat template | stock Qwen3 (`<|im_start|>`), no BOS, EOS `<|im_end|>`; tokenizer and template byte-identical to the 8B's (3.2) | DeepSeek-V3 style: text starts with `bos_token` (`<|begin▁of▁sentence|>`, id 100000) + system, then `<|User|>{user}<|Assistant|>`; `add_bos_token = true`; EOS `<|end▁of▁sentence|>` (100001) | | user prompt | the Goedel prompt, byte-identical to the 8B: `Complete the following Lean 4 code:\n\n```lean4\n{stmt}```\n\nBefore producing the Lean 4 code to formally prove the given theorem, provide a detailed proof plan outlining the main proof steps and strategies.\nThe plan should highlight key ideas, intermediate lemmas, and proof structures that will guide the construction of the final formal proof.` | the same CoT prompt text, but the card's fence is `{stmt}\n```` (a newline before the closing fence); Goedel's is `{stmt}```` with none. The two are therefore NOT byte-identical: `goedel` format on DeepSeek is a one-newline deviation from the card, to be pinned and recorded (or add a `deepseek` format that inserts the newline; prefer that, 5 lines) | | statement shown | `GOEDEL_HEADER` + `...:= by sorry` | card example: `import Mathlib\nimport Aesop\n\nset_option maxHeartbeats 0\n\nopen BigOperators Real Nat Topology Rat\n\n` + optional `/-- informal -/` docstring + theorem `:= by\n sorry` (same header as `GOEDEL_HEADER`) | | output | plan prose, then a final ```lean4 block | plan prose, then a final ```lean4 block | | sampling (card / harness) | temperature 1.0, top_p 0.95 (Goedel protocol; what `lean_gen` hard-codes by default) | card gives none; use the same 1.0 / 0.95 for comparability | | context | 40,960 (Qwen3) | config 65,536 (YaRN x16, orig 4,096), card says 32K: run with `--max-model-len 32768`, cap <= 30,000 | | repair prompt | the Goedel round-k correction prompt works as for the 8B (model is the same family and trained for it; 90.4% "self-correction mode") | none published; round 0 only | The Goedel format of `extras/vllm_rollout/lean_prompts.py` serves Goedel-32B exactly (same family, same template) and DeepSeek-7B up to the one-newline fence difference above. What is needed is in section 5. ## 5. Design note: changes for `extras/vllm_rollout/` (another lane's files; NOT edited here) 1. **Double BOS for DeepSeek (correctness).** `lean_gen.prepare()` does `ids = list(tokenizer.encode(text))` (default `add_special_tokens=True`) on text produced by `apply_chat_template(tokenize=False)`. For Qwen3 that is a no-op (no BOS). The DeepSeek template already prints `bos_token`, and `encode` would prepend a second BOS id 100000. Change: a `bos_in_text = tokenizer.bos_token is not None and text.startswith(tokenizer.bos_token)` test and `tokenizer.encode(text, add_special_tokens=not bos_in_text)`; apply the same expression in `run_fidelity`'s id check and in `prompt_render_sha` (so the sha pins the ids actually sent). Note `lean_gen` sends `prompt_token_ids`, not text, so vLLM adds nothing itself. Without this fix DeepSeek-7B generates with a doubled BOS: a silent quality loss, not an error. 2. **Teacher provenance in every record** (needed for the license ledger, section 7). `build_record(..., extra=...)` already takes `extra`: add `--teacher-id` (hub repo), `--teacher-license` (hub string) and `--teacher-card-rev`; write `{"teacher": {"repo", "revision", "license", "role": "teacher"}}` into each record and into `write_manifest`. `model_rev_of` already supplies the snapshot sha. Make `resume_conflicts` refuse to resume a file under a different teacher id. 3. **Engine flags for the 32B**, as CLI options passed through to `LLM(...)` (today `LLM(...)` is called with a fixed kwarg set): `--kv-cache-dtype {auto,fp8}` (default auto), `--enable-chunked-prefill` (leave vLLM default), and a default `--max-num-seqs 16` for the 32B (128 is the 8B default; with room for ~12 sequences a larger value only grows the preemption/recompute churn). `--gpu-mem-util 0.94` is worth one smoke try, with the NVML guard the lane already has. 4. **`--max-model-len`**: default 40,960 is right for Goedel-32B (Qwen3 limit) and **wrong** for DeepSeek-7B (use 32,768). The existing check "max_model_len above the model's position limit raises" must read `max_position_embeddings=65536` yet the card limit is 32K: add an explicit `--max-model-len 32768` in the launch command rather than trusting the config. 5. **No change** to extraction (`extrac_code`, last ```lean4 fence), `lean_code_for_record`, `problem_check` or the sampling defaults: both teachers answer in the Goedel output shape. The `kimina` format and `kimina_final_answer` are irrelevant here. 6. **Cap**: use `--cap-tokens 16384` for the first pass. Goedel reports their protocol at 32k; at cap 16k the 8B hit the cap in 8.2% of miniF2F samples and the hard band will be higher (assume ~35%). Rejected-by-cap samples are not proofs and cost the most, so a second pass at 32k only on never-solved statements (section 6). 7. **Sampler parity**: pass `generation_config="vllm"` to `LLM(...)` (or explicit `top_k`) so a model's own `generation_config.json` (3.2) cannot change the sampler. ## 6. Plan: what each teacher attempts Definitions (from the Phase-0a supply audit, `scripts/p2/supply/`): the band is every pool statement that passed the elaboration check and the misformalisation screen, with a profile of OUR 8B at n = 16-32. * **Marginal band M**: 8B pass rate in (0, 0.25]. Real proofs exist, the 8B finds them rarely; a stronger teacher's proofs add style and a higher hit-rate signal. Highest expected yield per token. * **Never-solved band N**: 0 / 32 for the 8B, and not flagged misformalised (Goedel: >80% of unsolved Pset statements were misformalised; a teacher "proof" of a vacuous or wrong-statement theorem is worthless, so the screen comes first). | stage | teacher | statements | n | cap | rate (est.) | mean tokens | box-h | expected yield | |---|---|---|---|---|---|---|---|---| | T0 smoke / calibration | Goedel-32B | 64 from M, round 0 | 2 | 16,384 | measure | measure | **~1.5** (incl. load ~4 min) | replaces every rate in this note | | T1 marginal | Goedel-32B | M (B_M, expect a few hundred) | 4 | 16,384 | ~145 tok/s | ~8,000 | `B_M x 4 x 8,000 / 145 / 3600`: **B_M=300: 18 h; 500: 31 h** | high: expect pass@4 of 30-60% on M | | T2 never-solved | Goedel-32B | N screened (B_N) | 8 | 16,384 | ~145 tok/s | ~11,000 | **B_N=300: 51 h; 500: 84 h** | low: single-digit % of N; each proof is a new problem | | T3 correction round | Goedel-32B, Goedel correction prompt | N statements with failures that have compile errors (feedback present) | 1 per failed parent | 16,384 | ~145 | ~9,000 | ~25% of T2 | adds the "self-correction" lift (88.0 to 90.4 on miniF2F, a reported figure) | | T4 (optional, license-gated) | DeepSeek-Prover-V2-7B | M + N | 4 | 16,384 | ~365 tok/s | ~7,000 | B=800: 15 h | different proof style; only if 1.1 is resolved | Budget rule of thumb: the 32B costs ~7 box-h per 1M generated tokens (145 tok/s), so the SFT-worth of T2 must be weighed against the SOTA_PLAN line items (Phase 1 ~200 box-h). **Recommended**: T0 then T1 (≈20-30 box-h), then T2 only on the screened subset of N with a prior (e.g. classifier score) and B_N <= 200 (~35 h), stop early with the lane's early-stop when yield per box-h drops below the 8B's own self-distillation yield. All stages run strictly serially with the 8B generation (hostguard lock: never two heavy jobs), and stage-checkpoint at every `lean_gen` flush (the writer already flushes every 8 records). Throughput honesty: 130-160 tok/s is the model's estimate with eff calibrated on the 8B. Two unmeasured risks: (a) NCCL all-reduce at 128 per step with only ~10 sequences is latency-bound and may be worse than eff 0.46 suggests (could be 100 tok/s); (b) fp8-KV on Ampere may be slower or unsupported. T0 resolves both. If T0 reads below ~100 tok/s, T1 halves (B_M x n = 600 samples) and T2 is dropped. ## 7. How teacher proofs enter training 1. **Verified only by OUR frozen verifier** (`docs/LEAN_VERIFIER.md`, hash-pinned): the teacher's own claim of success, its own compile feedback, and any third-party verifier are ignored. The accepted set is exactly what `verify_generations.py` accepts at the frozen verifier hash (300 s, axiom walk, statement-fidelity check, `problem_check` re-insertion of the ORIGINAL statement). 2. **Decontaminated before training** with the same pipeline as every other source (`align.data.decontam`, elaborated-statement hashing + MinHash + the base-model membership check): the PROBLEM must be absent from miniF2F valid/test, ProofNet, PutnamBench and every eval set; teacher output does not launder a contaminated statement. Teachers were trained on data we do not control (Goedel-V2 on its own Workbook/Pset; verify the Goedel-Pset overlap with the pool statement sha, not just by name). Proof text is also MinHash-checked against the eval proof sets (miniF2F solution zips) so a memorised published proof cannot enter. 3. **Provenance on every row**: `{source: "teacher", teacher_repo, teacher_revision, teacher_license, card_revision, prompt_format, temperature, top_p, cap, seed, verifier_hash, problem_ancestry}`. `build_sft_data.py` gets a `--source-tag` so teacher rows are a separate, countable stream; per-problem cap (`--max-per-problem 4`) applies across self + teacher rows. 4. **Separate arm in the contrasts**: teacher rows are added as their own SFT arm (self-only vs self+teacher, same token budget) so a gain is attributable and a loss is not hidden by the pooled mix. Same eval, same verifier, same seeds, matched row counts; the teacher stream is registered in the pre-registration BEFORE generation (docs/PREREG_SOTA.md) with its pass/fail gate (it must not lower self-distillation accepts/box-h). 5. **Licenses recorded and checked at release**: `docs/TEACHERS.md` (this file) plus a machine-readable ledger `teachers_ledger.json` written by the lane from the record `teacher` blocks; the release script refuses a model whose training mix has a teacher with a non-permissive or unresolved license (DeepSeek until 1.1 is decided). Goedel-V2-32B is Apache-2.0 with no output clause: attribution only (NOTICE entry). Distillation from a model whose own base is Qwen3-32B (Apache-2.0) adds no further restriction. 6. **Selection**: drop teacher proofs whose length exceeds the SFT sequence budget (16,384), those that hit statement-rewrite paths, and those that use `sorry`/axiom tricks (verifier already rejects). Keep at most 2 distinct proofs per problem from the teacher (diversity over volume), shortest-accepted-completion first as the 8B pipeline does.