/teal-sea
teal-sea / zeta-labstate of record · compiled 02 Nov 2026 · revision b09fa4dd · source

Library · hunts/rung3_ball_production/PRODUCTION.md

PRODUCTION: runbook for the 215 sites of plan v2 and their assembly

2,455 words · 274 lines · source

Hunt #145, written 2026-10-26, revised the same day after the whole-tower re-pilot. The owner granted the production window on cloud compute that day (MISSION.md, "Owner decision, 2026-10-26"). The Novita account had zero balance, so nothing here has been launched. This page is what the coordinator runs once compute is funded. Every cost below is a projection from one pilot site (PILOT.md sections 4 and 9), not a measurement of production.

0. Decide before launch

  1. Use --tower whole. The generator now has an opt-in whole-tower mode (scripts/60 --tower whole). Each prime tower is one decide +kernel of dirichletTermBallB ... = lit plus contains_dirichletTermBallB. The pilot site compiled in it end to end with both planted tower faults refused (PILOT.md section 9). Projected: 30 core-hours (29.8) against 416 core-hours for the default staged mode, and 14.8 to 16.0 GB of oleans against 123 to 146 GB.
  2. Where. On these projections the sites fit GitHub Actions shards and the assembly fits one 32 GB machine (section 1). That is assessed, not tested. One single cloud machine is the simpler alternative.
  3. One thread per process. scripts/72 runs lean at its default threads. -j1 buys predictable sharing and no saving (OWNER_MEMO.md, item 3).

1. Machine shape

resource--tower whole (recommended)staged (default)
CPU, 215 sites29.8 core-hours; largest site the centre, 2,173 CPU-s416 core-hours; the centre 31,817 CPU-s
wall clock, 32 dedicated coresabout 1 h plus generationabout 13 to 19 h
RAM per compile processpeak RSS 6.5 GiB, most of it Mathlib's oleans in the shared page cachepeak RSS 7.8 GiB, at most 1.91 GiB anonymous
oleans, 215 sites14.8 to 16.0 GB (centre at most 0.37 GB)123 to 146 GB
assembly, one processabout 17 to 18 GB RSS: 6.7 GB of support modules plus 0.70 x the oleans81 to 96 GB above the support modules
diskabout 16 GB of oleans, 0.19 GB of sources, 7.6 GB of Mathlib builda 250 GB volume

Where production fits, in whole mode (PILOT.md section 9 gives the reasons):

Never run Lake in a copied .lake (CLAUDE.md, compute rule 7).

2. The workflow: .github/workflows/rung3-production.yml

The sites run on GitHub Actions by the workflow, which production.py drives step by step (test_production.py pins both). It is triggered only by a push of RUN_REQUEST.json to hunt/rung3-ball-production, never by the workflow file, and only after the coordinator's 'go'. Limits: permissions: contents: read, a concurrency group, 350 minutes per job with compiles stopping themselves at 300, and at most 6 jobs at once.

Each job removes unused runner toolchains to free disk, then restores ~/.elan and ~/.cache/mathlib (Mathlib's downloaded archives, keyed on revision 065356127b1d). It builds a fresh lean/.lake with lake exe cache get and never restores a .lake. It checks that every package is its own git toplevel. It runs the fail-closed preflight: pinned toolchain, Mathlib revision, request shape, and the staged default's byte identity against the 2026-10-21 hashes. Then it builds the ZetaLean closure, generates its sites (scripts/60 --tower whole), compiles them (scripts/72) and audits the axioms. It uploads rung3-production-job<N>-attempt<A> (oleans, checkpoints, manifests, logs) and a small rung3-production-record-job<N>-attempt<A>. A re-run attempt downloads its job's earlier artifacts and resumes from their checkpoints.

The planted-fault path and the axiom audit ran on this box against the whole-mode pilot site before the workflow was pushed. The fault was refused at Prime_2.ev_whole, and at Prime_B_right_06_2.ev_whole after the rename of PILOT.md section 11. upper_B_right_06 printed exactly the three axioms. The calibrate mode then ran on a runner and succeeded (run 40340279740, PILOT.md section 10): the centre took 1,348 CPU-s and 557 s of wall at an 11.3 GB peak, with a 295 s setup. On runners, production is about 18.5 core-hours. The full mode has not run; it needs its own 'go'.

2a. Commands, for a single machine instead

From a fresh clone of the branch commit, on the production machine (or in each job):

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt && .venv/bin/pip install -e .
cd lean && lake exe cache get          # production machine only, never on the shared box
for p in .lake/packages/*; do git -C "$p" rev-parse --show-toplevel; done   # each names itself
lake build ZetaLean.BallCertSupport ZetaLean.BallTerm   # the 15 ZetaLean modules the sites import
cd ..

Generate the sites. This is Python only: the census of all 215 sites in whole mode took 51 CPU-minutes, the centre 69 s and a grid point about 20 s. It parallelises per site with --only <id>:

GEN=/data/rung3/gen OUT=/data/rung3/out
.venv/bin/python scripts/60_rung3_generate.py lean/cert/rung3_plan2.json "$GEN" \
  --arith ball --shard --tower whole            # or --only <id>, once per site of a job
ls "$GEN"/*.manifest.json | wc -l      # 215 on one machine

Compile, resume-safe. --jobs (sites) times --module-jobs (modules per site) is the number of concurrent lean processes. Keep it at or below the cores (2 on a 16 GB runner), and keep MemAvailable above 20 GiB on a large machine:

nice -n 10 .venv/bin/python scripts/72_rung3_ball_compile.py --lean-project lean \
  --output-root "$OUT" --jobs 24 --module-jobs 1 "$GEN"/*.manifest.json

3. Checkpoints

Then the assembly, in one process, with every site root's olean on LEAN_PATH (they are linked beside their sources in $GEN):

A=/data/rung3/assembly
.venv/bin/python hunts/rung3_ball_production/assembly.py --out "$A" --check   # emits; the pin must match
H=hunts/rung3_ball_production/lean
cd lean
lake env lean --root=../$H -o "$A/Rung3Assembly/Support.olean" ../$H/Rung3Assembly/Support.lean
lake env sh -c "LEAN_PATH=$A:$GEN:\$LEAN_PATH lean --root=$A -o $A/Rung3Assembly/Assembly.olean $A/Rung3Assembly/Assembly.lean"

Assembly.lean and Separation.lean are emitted, not committed: they import the generator's site roots, which are not module files (PILOT.md section 8).

Separation.lean also needs hunt #137's olean closure (hunts/rh_height_lean/lean/build.sh --full, built against lean/vendor/zeta23's Mathlib at the same revision) on the same LEAN_PATH. That combination has never been run; if the two Mathlib builds conflict, record it and keep dh_separation_of, which takes RH to height 100 as a hypothesis.

4. Planted faults to re-run on production output

On one compiled big-box site (B_right_06, as in the pilot), with pilot.py's work directory set by RUNG3_PILOT_WORK. Each must be refused at the named declaration, and the clean site must still compile:

export RUNG3_PILOT_WORK=/data/rung3/faults
.venv/bin/python hunts/rung3_ball_production/pilot.py site --site B_right_06
.venv/bin/python hunts/rung3_ball_production/pilot.py fault --site B_right_06 --kind tower-rad   # a tower radius 1 unit of 2^-64 low
.venv/bin/python hunts/rung3_ball_production/pilot.py fault --site B_right_06 --kind tail-rad    # the tail radius just under its bound
.venv/bin/python hunts/rung3_ball_production/pilot.py fault --site B_right_06 --kind M           # M just under the enclosure's bound
.venv/bin/python hunts/rung3_ball_production/pilot.py centre-eps                                 # eps' refused at and below the bound

In whole mode, also re-run the two tower faults on that site (whole_pilot.py faults with RUNG3_WHOLE_WORK pointing at a whole-mode compile of B_right_06 made by whole_pilot.py site). Each must be refused at the faulted tower's ev_whole. The four pilot.py faults above run on the staged sources that pilot.py site generates. The tail-rad, M and centre faults sit in modules that whole mode leaves unchanged.

5. Axiom audit

Only after that audit does davenport_heilbronn get a grade, through the hunt's case log and review (ALIGNMENT.md). It is a statement about the Davenport-Heilbronn function and says nothing about RH (docs/08).

6. The assembly: collect, check, import (the coordinator decides placement)

Prerequisite: whole-mode site oleans with site-qualified tower namespaces (Prime_<sid>_<p>, PILOT.md section 11), from a full run whose verdict job counted it. Output of the staged default, or of whole mode before that fix, cannot be imported two sites at a time.

Size, re-projected from the runner. The calibration's centre measured 343.5 MB of oleans, inside the projected 316 to 371 MB, so the projected 14.8 to 16.0 GB total stands until the full run's records give the exact sum. Importing the runner-built centre raised peak RSS by 0.708 bytes per olean byte over the 6.57 GB of the support imports (import_probe_centre). By that ratio the 215-root import needs 10.4 to 11.3 GB above the support modules, about 17.0 to 17.9 GB in all, nearly all of it mapped olean pages. Still untested: one process importing all 215 roots. The two-site harness (section 8 of PILOT.md) is the largest real import so far.

Collect and check. On the assembly machine, from this checkout:

D=/dev/shm/rung3-assembly       # or a directory on the cloud machine's disk
.venv/bin/python hunts/rung3_ball_production/production.py collect \
  --request hunts/rung3_ball_production/RUN_REQUEST.json --run <full run id> --dest "$D"

collect takes each job's latest-attempt rung3-production-job<N>-attempt<A>. It downloads the zip through the API and checks its SHA-256 against GitHub's recorded digest, refusing on any mismatch. It unzips into $D, deletes the zip before the next one (so the peak is the oleans plus one zip), and checks that every site the job owns has its root olean and its root checkpoint. It writes $D/collected.json with every artifact's hash. Then:

The oleans are trusted as the runners' kernel left them; the import does not re-check their proofs. For an independent re-check, run the toolchain's leanchecker over the assembly's environment. That replays every declaration in the kernel, at a cost of about the sites' compile CPU; it is not yet sized.

Import. Emit and compile, with $D/out (all jobs' outputs merged, file names disjoint by site) on LEAN_PATH:

.venv/bin/python hunts/rung3_ball_production/assembly.py --out "$D/asm" --check
cd lean
lake env lean --root=../hunts/rung3_ball_production/lean -o "$D/asm/Rung3Assembly/Support.olean" \
  ../hunts/rung3_ball_production/lean/Rung3Assembly/Support.lean
lake env sh -c "LEAN_PATH=$D/asm:$D/out:\$LEAN_PATH lean --root=$D/asm \
  -o $D/asm/Rung3Assembly/Assembly.olean $D/asm/Rung3Assembly/Assembly.lean"

Then run the axiom audit of section 5 on ZetaLean.DH.Rung3Assembly.Plan2.davenport_heilbronn.

Placement options, against about 17 to 18 GB of resident memory and about 16 GB of oleans:

  1. The shared box, with the oleans on a RAM-backed tmpfs. Its disk sits below the co-tenant's floor, so $D is /dev/shm/rung3-assembly, which is already a 58 GB tmpfs and needs no sudo. This checkout's existing lean/.lake serves Mathlib, so no new Mathlib is fetched. The mapped oleans are the tmpfs pages themselves and are not counted twice. The total is about 16 GB of tmpfs, Mathlib and support oleans in page cache (about 7 GB, mostly resident already) and a few GB of anonymous memory.
  2. Start only while MemAvailable is above 45 GiB (the 20 GiB guard plus about 25 GiB) and the 1-minute load is at most 64.
  3. Guard: read MemAvailable every 10 s. Below 20 GiB, stop the Lean process and rm -rf /dev/shm/rung3-assembly. A tmpfs is lost on reboot, so the artifacts stay on GitHub (30 days) until Assembly.olean exists.
  4. Cost: the harness's import took 127 CPU-s with two real sites; the full import is projected under 30 core-minutes, within this box's one core-hour cap.
  5. Decision: the 2026-10-26 owner decision reserves the box for pilots, so placing the assembly here is the coordinator's or owner's call.
  6. A funded cloud machine with at least 32 GB of RAM and 40 GB of disk, using a fresh lake exe cache get. It has no co-tenant risk, but it waits for funding.

The calibration's centre olean bytes already checked the projection once. The full run's olean_gb in its verdict summary checks it again before the import starts.