| Field | Value |
|---|---|
| Decision ID | ADR-091 |
| Initiative | verification capacity / throughput |
| Proposed By | unsorry maintainers |
| Date | 2026-06-24 |
| Status | Proposed |
Discussion #5656 (and the roadmap issue #5678 / #5682) records the current Gate A throughput ceiling: a flat ~24 proof-merges/hour with no headroom, where each green PR costs ~10–12 namespace runner-minutes and the axiom audit is ~55–65% of that (~360–443 s) — by far the largest single chunk, and grown ~2.5× from the ~165 s measured in SPEC-049-A §5.6 as the verified library scaled. The kernel replay long pole was already cut by ADR-063 (sharded across N matrix runners, ~1/N wall-clock) and is now cheap; the axiom audit is the remaining unsharded straggler and the concurrency choke.
The audit driver (tools/gate_a/parallel_modules.py::audit) already chunks the
work (split_evenly) and supports intra-process parallelism
(ThreadPoolExecutor, bounded by max_safe_jobs), and its scope is already
incremental (changed library closure + changed goals, scoped_audit_targets,
fail-closed via forces_full_audit). But the required gate-a.yml invokes it with
--jobs 1: each axiom_audit process loads the full mathlib image (~6–7 GB)
to run collectAxioms, so two concurrent invocations OOM a single runner — exactly
the per-runner memory constraint ADR-063 faced for leanchecker. That constraint
is intra-runner; it says nothing about auditing disjoint module subsets on
separate runners. SPEC-063-A §6 already names audit sharding as the documented
fast-follow (“same planner; even safer”).
The load-bearing soundness invariant is unchanged (ADR-048/049): every
in-scope module is axiom-audited against the whitelist {propext,
Classical.choice, Quot.sound} (∪ sorryAx for goals), from a locally-derived
trusted-CI build, never from a client artifact. ADR-058 governs verification
capacity and requires a non-required shadow pilot before any change to
required-check routing.
In the context of a Gate A axiom audit that is already chunked, incrementally
scoped, and order-independent, yet pinned to --jobs 1 on one runner only because
each axiom_audit process holds mathlib resident — making it the now-dominant
~55–65% of per-PR verifier cost and the binding constraint on the ~24-merges/hour
throughput ceiling (#5656/#5682),
facing the need to raise throughput without weakening the every-module-audited invariant (ADR-048/049), without trusting any client-supplied artifact, and without changing the required-check contract before it is piloted (ADR-058),
we decided for sharding the axiom audit across N parallel matrix runners,
mirroring ADR-063 verbatim for the audit lane: a new compute_audit_targets
(factored out of audit as the single source of truth, returning an AuditScope
of {full | incremental | none} over both library and goal modules, reusing
scoped_audit_targets / forces_full_audit unchanged) feeds a new
plan_audit_shards that emits an N-way matrix index list, and a new
audit_shard re-derives the same scope from source on each leg and audits
only split_evenly(library ++ goals, N)[i] — separating its slice back into the
plain (axiom_audit) and --allow-sorry (goal) invocations — so each leg shares
nothing but the git SHA (no module list crosses a job boundary, keeping the
auditor’s inputs locally-derived). Because split_evenly is disjoint and
covering (unit-tested) and the per-module audit verdict is independent, all
shards green ⟺ every module audited exactly once, at ~1/N wall-clock; a
combine_audit_reports concatenates the per-shard axiom-report.json
fragments into the unified footprint report. Gated by a cover job (fail-fast:
false matrix + a cover that fails closed unless every shard is green, or count ==
0 vacuously) and the unchanged daily full-audit backstop; rolled out
non-required first via an audit leg in the existing gate-a-shard-pilot
workflow before promotion into the required gate-a.yml (ADR-058),
and neglected lifting the per-runner --jobs cap to run two audits on one fat
runner (rejected as the primary fix — bounded by one runner’s RAM and OOM-prone,
the same reason --jobs 1 exists today; cross-runner sharding is the
unbounded, memory-safe parallelism, though a fat-profile shard may still use
intra-runner jobs later), passing a precomputed shard plan between jobs as an
artifact (rejected — re-deriving the slice on each leg from the shared SHA keeps
the auditor’s inputs locally-derived and sidesteps the ADR-049 client-artifact
footgun), promoting the matrix straight into the required gate (rejected — ADR-058
mandates a non-required pilot first, and the empty-matrix/skip and matrix-expansion
behaviours need real-runner validation), and simply buying bigger/more runners (an
operator capacity lever, orthogonal to per-run wall-clock and not a repo change),
to achieve an ~N× cut in the now-dominant audit cost so Gate A throughput scales with the operator’s runner budget instead of one serial audit, lifting the ~24-merges/hour ceiling (#5656),
accepting that sharding introduces a bookkeeping risk identical in kind to
the existing ADR-048 incremental-scoping risk — a planner bug that drops a module
would let it reach main un-audited — bounded three ways: the partition is
unit-tested disjoint+covering, the cover job fails closed on any non-green shard,
and the daily full-audit backstop re-derives soundness within 24 h and goes red on
any gap; that the required-gate promotion is a separate, pilot-gated step (the
pilot leg ships first); that the shard count N is a new operator capacity knob
(vars.UNSORRY_AUDIT_SHARDS, separate from UNSORRY_REPLAY_SHARDS) spending N
parallel verifier runners (ADR-058 governance), so peak concurrent runners per PR ≈
replay + audit shard counts; and that the audit cover job becomes
soundness-load-bearing and joins the CODEOWNERS TCB (ADR-019).
| Ships now (this ADR / SPEC-091-A) | Deferred |
|---|---|
compute_audit_targets + plan_audit_shards + audit_shard + combine_audit_reports (reuse the verbatim scoping logic) |
Intra-runner --jobs > 1 on a fat audit-shard profile |
| Unit tests: disjoint+covering partition (library ∪ goals), fail-closed-to-full, no-op empty matrix, out-of-range no-op, failure propagation, composable-fragment | Sharding the daily gate-a-full-replay/full-audit backstop |
Audit leg in gate-a-shard-pilot — non-required manual validation on real runners |
— |
Promotion of the audit matrix into the required gate-a.yml (after the pilot is green) |
— |
The serial audit() (--jobs 1) path is unchanged and remains the
fallback/backstop.
| Reference ID | Title | Type | Location |
|---|---|---|---|
| REF-1 | Sharded Gate A axiom audit spec | Specification | specs/SPEC-091-A-Sharded-Gate-A-Axiom-Audit.md |
| REF-2 | Sharded Gate A Kernel Replay | Decision | ADR-063-Sharded-Gate-A-Kernel-Replay.md |
| REF-3 | Runner-Pool Segmentation and Verification Capacity | Decision | ADR-058-Runner-Pool-Segmentation-And-Verification-Capacity.md |
| REF-4 | Verify-on-Ingest | Decision | ADR-048-Verify-On-Ingest.md |
| REF-5 | Incremental Kernel Replay | Decision | ADR-033-Incremental-Kernel-Replay.md |
| REF-6 | Decentralised CI Runner Architecture | Decision | ADR-049-Decentralised-CI-Runner-Architecture.md |
| REF-7 | Gate A Workflow | Specification | specs/SPEC-006-B-Gate-A-Workflow.md |
| REF-8 | Verification-throughput roadmap | Discussion | GitHub discussion #5656 |
| REF-9 | D1a — shard the axiom audit | Issue | GitHub issue #5682 (roadmap #5678) |
| Status | Approver | Date |
|---|---|---|
| Proposed | unsorry maintainers | 2026-06-24 |