Skip to content

Calibrate the orbits stack: thread-gated prepared paths, planner re-fit, prover integration - #217

Merged
ebfull merged 8 commits into
mainfrom
orbits-calibration
Aug 26, 2026
Merged

Calibrate the orbits stack: thread-gated prepared paths, planner re-fit, prover integration#217
ebfull merged 8 commits into
mainfrom
orbits-calibration

Conversation

@ebfull

@ebfull ebfull commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Re-calibration of the orbits stack against fresh interleaved grids on Skylake-X (16C/32T) and Apple M4 Max (12P+4E), and prover-side consumption of the prepared fixed-base tables. Every behavioral change is measured on both hosts. Two posture decisions frame the series: the orbits cargo feature is now opt-in (default-off) until the planner's remaining per-arch conflict zones settle, and every prepared route is thread-gated so arming is never a pessimization anywhere we measured — the shape we want it in before it ever becomes default-on.

Split note: the x86-64 assembly field backend originally in this series now lives in #222, so it can coordinate with the in-flight x86 squaring work in #218 without holding this PR up. Nothing here depends on it — where the planner docs cite calibration data gathered on the assembly grids, they now say so generically. The branch is also rebased onto current main (#220, #221).

What changed

Planner bandwidth floor (pasta)

Parallel backend estimates are floored at 8% of their total group-operation count: wide pools divide per-worker work but not memory traffic. The floor shapes the orbit backend's window width on any parallel pool and joins the backend-versus-backend comparison only past 16 workers (at 16 the mid-size Booth/orbit boundary measures in opposite directions on the two hosts, so the comparison deliberately stays unfloored there — evaluated at the floor-picked orbit width). Summed planner cell losses: 269 → 136 pp (grid with the #222 assembly field backend), 353 → 176 pp (portable). End to end this fixed both shipped verifier regressions: x86 k=16 (−8% → +8%, measured with the assembly backend enabled) and M4 k=15 (−4..−11% → parity, via the width-5/6 boundary landing at ~28,672 terms). Pinned in multiexp_plan_selection.

Thread-gated prepared routing (halo2)

MSM::eval uses an armed prepared zero-check only on pools of at most PREPARED_MSM_MAX_THREADS (8) effective threads. The prepared evaluation stops scaling past that width while the unprepared planner keeps scaling: armed verification measured +14–16% slower at 16T (M4) and +22–27% at 32T (Skylake-X) before the gate, and is parity there now, while keeping the 8–22% wins at 4–8T. Validated within-process on both hosts. The gate is one constant, deliberately shared with the prover routes below.

Prover integration (pasta/halo2/orchard)

PreparedZeroCheck::multiexp_with_terms_vartime returns the exact MSM the zero-check already evaluates; Params::prepare_commitments arms it for commit ([g…, w, u], shared with the verifier's table; scalars [poly…, r, 0]) and commit_lagrange (a second [g_lagrange…, w, u] table) behind the same 8-thread gate; orchard::ProvingKey::prepare_proving exposes arming. Measured 1.2–1.8x per commitment at 1–8 threads on both hosts across full-width and witness-like scalar mixes, and +3.5–11% end-to-end proving at 8T for k≤14. One noisy cell: k=15 armed at 8T read −5.6% (lean budget mode at 32,770 bases; single run near the ±3% noise floor) — worth a cross-check before anyone adds a size guard. (This commit originally also parallelized the advice/h(X) commitments; #212 landed that independently while this PR was open, and the rebase keeps #212's version.)

orbits now default-off (halo2)

Removed from halo2_proofs's default features (it was never in pasta's own defaults). Downstream builds on default features get the pre-series Signed-Booth planner and plain final identity test, byte-for-byte the old behavior. Params::prepare_zero_checks / prepare_commitments and orchard's prepare_batch_validation / prepare_proving stay present as no-ops returning false, so callers never need feature-gating. CI's Linux stage job enables orbits explicitly for runtime coverage; the 32-bit and platform-smoke jobs cover the (now default) off state.

Post-review cleanup (final three commits)

A dead test shim that warned on every non-test orbits build; the planner's generic-traffic formula folded into the Booth cost model instead of silently mirroring it; prepare_commitments made idempotent with no partial-arm states; the prepared-gate constant renamed to cover all three routes it now gates, with rustdoc naming the constant instead of hardcoding "eight" in four places; the gate tests pinned under capped pools at the gate width and one past it (CI runners are 2–4 cores and never exercised the armed fall-through before); changelog entries that narrated the branch's history merged into their unreleased base entries.

Design posture

  1. Ready to be default-on later: with the thread gates, arming (and eventually default-enabling) never regresses any measured configuration — wide pools transparently keep the planned multiexp, narrow pools take the wins.
  2. Easy to remove or replace: everything new sits behind orbits (pasta forwards it as orbits = ["glv"], halo2 forwards pasta's); the arming APIs are stable no-ops without it.

Deliberately deferred

Per-arch conflict zones left at today's boundaries: the 16-worker mid-size backend choice and witness-shaped selection at 8–16 workers (opposite signs on the two hosts), and the x86 k=16 one-shot residual (isolated grids show orbit winning that cell; in-verifier it doesn't — steady-state vs cold-context divergence). The commit gate reads pool size, not the effective per-commit share under concurrent commits; an effective-threads hint would let wide pools use prepared commits too.

Validation

Full suites green in every feature state on both hosts before the split (pasta 200/196/190/142, halo2 91 + plonk_api with and without orbits, orchard 163/204); after the split and rebase, re-run on the x86 host: pasta 196 (multicore,orbits) and 218 (--all-features, which no longer includes the assembly), halo2 full suite plus the commitment/msm lanes with batch,orbits, rustfmt clean. Planner: an offline replica reproduces the Rust planner 100/100 on grid cells pre- and post-fit; per-cell regression checks on both grids; M4 plan diff byte-identical at ≤16T except the two intended width changes; the costs-model refactor is pinned by the measured-cell plan tests. Gates and prover effects validated within-process or with alternated rounds; the armed fall-through is now pinned by capped pools rather than host width. The halo2/orchard/glv trees are byte-identical to the previously validated branch tip apart from the rebase and two doc rewordings in glv.rs.

🤖 Generated with Claude Code

ebfull and others added 8 commits August 26, 2026 16:56
The prepared evaluation stops scaling past ~8 workers (fewer window
tasks than the unprepared orbit backend; reduction work inflates under
contention) while the unprepared planner keeps scaling, so on full pools
the armed identity test measured slower end-to-end: +14-16% at a
16-thread pool (Apple M4 Max) and +22-27% at a 32-thread pool
(Skylake-X) at k = 11, against wins of 8-22% at 4-8 threads on both
machines. eval now routes through the prepared check only on pools of at
most PREPARED_ZERO_CHECK_MAX_THREADS (8) effective threads and falls
back to the plain planned multiexp above it, so arming is never a
pessimization.

Validated within-process on the Orchard-shaped verifier bench: at the
32-thread pool armed is now parity with unarmed (was +22-27%); at an
8-thread pool armed wins -20/-21/-9/-1% at 1/2/16/64 actions. The
placement test pins the prepared path inside a capped pool so it keeps
testing the preparation on hosts of any width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wide pools divide per-worker work but not total memory traffic, so past
window saturation the parallel estimates under-priced total data
movement: the planner picked orbit width 5 where width 6 measured 5-13%
faster at 65,536 terms on 16-32 workers (both curves, both grids — the
per-MSM face of the end-to-end k=16 verifier regression) and width 5
where width 4 measured up to 28% faster at 512-2,048 terms on 32
workers.

Each backend's parallel estimate is now floored at 8% of its total
group-operation count. The floor shapes the orbit width choice on any
parallel pool and joins the backend-versus-backend comparison only past
16 workers: at 16 workers the mid-size Booth/orbit boundary measures in
opposite directions on 16-core/SMT x86 and Apple M4 Max, so it
deliberately stays at the unfloored boundary there pending
per-architecture calibration. Inert at 8 or fewer workers by
construction; serial and low-worker plans are byte-identical.

Fit offline against the 2026-08-26 interleaved msm_backend_timings
grids (portable and x86_64-asm): summed planner cell losses drop
268.7pp to 136.3pp (asm) and 352.8pp to 176.2pp (portable) on the fit
data, with every plan change either a measured win (up to -28% per
cell) or within noise; a fresh grid run confirms the Rust planner
reproduces the fitted model's plan in all 100 cells.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bandwidth floor moves the 16-worker width-5/6 boundary to ~28,672
terms, between grid points, so the k = 15 verifier's 32,770-term final
check plans width 6: plan(32_768, 16) = Orbit{6} is now pinned. Measured
end to end on M4 Max as that verifier's ~5% orbit loss becoming parity
(and k = 16 an 8% win), with 16,384/24,576 staying at width 5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pasta: PreparedZeroCheck (and PreparedZeroMsm) gain
multiexp_with_terms_vartime — the exact multiscalar multiplication the
zero-check already evaluates, returning the group element instead of
comparing it against the identity. The zero-check becomes a thin wrapper
and the naive defensive fallback returns the point.

halo2: Params::prepare_commitments builds prepared tables over
[g..., w, u] (shared with prepare_zero_checks — commit's bases are a
subset with the blind riding w and u unused) and [g_lagrange..., w, u];
when armed, Params::commit and Params::commit_lagrange evaluate through
them behind the same eight-thread gate as MSM::eval, so arming is never
a pessimization. Measured 1.2-1.8x per commitment at 1-8 threads on
x86-64 and Apple silicon across full-width and witness-like coefficient
distributions, +3.5-11% on end-to-end proving at an 8-thread pool for
k <= 14. (An earlier version of this change also parallelized the
advice and h(X) commitments; #212 landed that independently and its
version is kept.)

orchard: ProvingKey::prepare_proving exposes arming for long-lived
provers, symmetric to VerifyingKey::prepare_batch_validation.

Tests: pasta pins the point-returning evaluation against the generic MSM
with and without extras; halo2 pins armed commit/commit_lagrange against
independent unarmed params on witness-like polynomials inside a capped
pool (and on the ambient pool for the fall-through), and the opening
proof round trip runs armed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
End-to-end calibration showed the stale parallel model over-selecting
the orbit backend on wide pools (verifier at k = 16 measured ~8% slower
orbits-on at 32 threads), so the series ships default-off until the
planner's thread-awareness settles. Runtime CI coverage of the prepared
zero-check path is unchanged: the Linux test job already lists orbits
explicitly, and the 32-bit and platform-smoke jobs cover what is now
the default orbits-off fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Test-gate the estimated_work shims (orbit and Booth): production goes
  through estimated_costs, so the wrappers were dead code in non-test
  builds, and the primary model docs now live on the costs functions.
- Fold the planner's inline generic-traffic formula into
  estimated_signed_booth_costs so the bandwidth floor's traffic input is
  definitionally in sync with the work model instead of silently
  mirroring it.
- Say precisely what the <=16-worker backend comparison does (unfloored
  work values, orbit at its floor-picked width).
- Refer to the calibration grids' field backends generically in the
  planner docs and changelog: the x86-64 assembly backend the grids ran
  on is PR'd separately now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Rename PREPARED_ZERO_CHECK_MAX_THREADS to PREPARED_MSM_MAX_THREADS and
  document all three routes it gates (MSM::eval's identity test and the
  commit/commit_lagrange prepared tables), and have the public rustdoc
  name the constant instead of hardcoding "eight" in four places.
- Harden Params::prepare_commitments: return early when the coefficient
  arm declines (no doomed Lagrange build, no partial-arm state the
  return-value doc misdescribes) and keep already-armed tables instead of
  rebuilding them, so repeat arming is free.
- Rename the Lagrange cache field/accessor to lagrange_table_cache /
  lagrange_table (it backs prover commitments, never a zero-check) and
  make ZeroCheckCache's type doc basis-neutral.
- Pin the armed wide-pool fall-through deterministically: the gate tests
  now run under capped pools at the gate width and one past it, instead
  of relying on wide hosts (CI runners are 2-4 cores and never covered
  the fall-through).
- Fold the standalone thread-aware-eval changelog entry into the
  unreleased prepare_zero_checks entry, whose old "up to ~2.4x on 8-16
  workers" claim advertised a regime the gate now refuses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ding

The "documents halo2's new thread-aware routing" entry described a doc
change to an API that is itself still unreleased — its substance now
lives in the prepare_batch_validation entry. The arming rustdocs phrase
the gate as "bounded-width pools (currently eight effective threads)" so
they read as point-in-time when halo2's per-arch recalibration moves the
constant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ebfull
ebfull force-pushed the orbits-calibration branch from 60e735d to 7206379 Compare August 26, 2026 23:02
@ebfull ebfull changed the title Calibrate the orbits stack: x86-64 field assembly, thread-gated prepared paths, planner re-fit, prover integration Calibrate the orbits stack: thread-gated prepared paths, planner re-fit, prover integration Aug 26, 2026
@ebfull

ebfull commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Per review: the x86-64 assembly field backend is now split out to #222 (so it can coordinate with #218), and the branch is rebased onto current main. That's what the force-push is — the halo2/orchard/glv content is byte-identical to the previously reviewed tip apart from the rebase and two doc lines in glv.rs that now refer to the calibration grids' assembly field backend generically instead of naming the x86_64-asm feature.

@ebfull
ebfull merged commit ff744d6 into main Aug 26, 2026
59 checks passed
@ebfull
ebfull deleted the orbits-calibration branch August 26, 2026 23:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants