Skip to content

Logup optimization - #65

Draft
gabriel-barrett wants to merge 2 commits into
mainfrom
logup-optimization
Draft

Logup optimization#65
gabriel-barrett wants to merge 2 commits into
mainfrom
logup-optimization

Conversation

@gabriel-barrett

Copy link
Copy Markdown
Member

Logup optimization: chained partial accumulators + message grouping

Shrinks the lookup argument's committed stage-2 trace from 1 + L to
max(1, ⌈L/k⌉) extension-field columns per circuit (L = lookups, k = a
new system-level lookup group size parameter), by replacing the per-lookup
inverse columns with a chain of partial accumulators whose steps each cover a
group of k messages.

The scheme

The old layout committed, per row, the running accumulator plus one inverse
column wᵢ = Mᵢ⁻¹ per lookup, constrained by wᵢ·Mᵢ = 1 and a separate
accumulator-update constraint.

The new layout threads the accumulator through the row: the committed
columns are the running accumulator acc plus one partial accumulator per
lookup group after the first, and each group (m₁, M₁), ..., (m_k, M_k)
contributes a single constraint

M₁ ⋯ M_k · (p − p_prev) = Σᵢ mᵢ · Πⱼ≠ᵢ Mⱼ

which forces the chain step p − p_prev to be exactly Σᵢ mᵢ/Mᵢ — the
denominators clear via m₁/M₁ + m₂/M₂ = (m₁M₂ + m₂M₁)/(M₁M₂), generalized to
k terms — without ever committing a message inverse. The final step of each
row lands on the next row's accumulator through the cyclic rotation.

Cyclic boundary

Instead of gating the final step behind is_transition and adding a separate
last-row constraint (which would multiply the message product by the
IsLastRow selector and cost an extra degree), the final step carries a
public correction term and holds on all rows:

M₁ ⋯ M_k · (acc_next − p_prev + is_last_row·(next_acc − acc)) = Σᵢ mᵢ · Πⱼ≠ᵢ Mⱼ

On ordinary rows the correction vanishes. On the last row the rotation wraps
to acc(0), and the telescoped sum around the cycle states exactly
next_acc = acc + Σ mᵢ/Mᵢ. Two consequences:

  • The accumulator column becomes a free gauge — defined up to a shared
    offset, with no first-row pin — since only the public difference
    next_acc − acc is enforced. (The witness still starts it at acc for
    clarity.)
  • The correction is a degree-1 selector times degree-0 public values, so
    every chain step has the same degree:
    max(1 + Σᵢ deg(Mᵢ), maxᵢ(deg(mᵢ) + Σⱼ≠ᵢ deg(Mⱼ))). No lookup constraint
    is gated by a row selector, and the degree profile is flat across rows and
    groups.

This requires the first/last-row selectors handed to the constraint folders
to take value exactly 1 on their row. The PCS returns them unnormalized
(l_first(1) = n, l_last(g^{n−1}) = n·g), which is fine for gating but not
for additive use, so the prover and verifier now rescale them by the matching
constants right after computing them. Scaling gated constraints by a nonzero
constant is semantics-preserving; the debug builder already used 0/1
selectors, so all builders now agree on one convention.

Degree accounting

With degree-1 multiplicities and degree-d messages, a group of k lookups
costs degree 1 + k·d (typically dominated by the product side). Examples:

messages k step degree fits at
deg 1 1 2 blowup 2
deg 2 1 3 blowup 2
deg 2 + deg 1 2 4 blowup 4
deg 2 + deg 2 2 5 blowup 4

API

  • System::new_with_lookup_group_size(config, airs, k) — the new
    constructor; the group size is applied uniformly to every circuit
    (consecutive lookups, final group smaller when k ∤ L).
    System::new(config, airs) is unchanged and means k = 1.
  • Admissibility is checked at setup: the existing constraint-degree
    assertion now fires for a too-aggressive grouping, so a bad k panics in
    the constructor — before any proving — with
    "...; increase log_blowup, lower the constraint degree, or reduce the lookup group size", instead of surfacing later as an out-of-domain
    evaluation mismatch.
  • LookupAir::from_parts(inner_air, lookups, preprocessed, lookup_group_size)
    — reassembles an air without recomputing preprocessed traces, for
    downstream verifying-key codecs (the group size is now a private field of
    LookupAir, set by the System constructor).
  • LookupValues::stage_2_traces takes the per-circuit group sizes; the
    prover's batch inversion shrinks to one inverse per group (the witness
    values are unchanged in meaning — the partial accumulators are the running
    sums the old scheme computed anyway).

Soundness

  • The multiset argument is unchanged: same messages, same accumulator
    telescoping, same N/|F_ext| Schwartz–Zippel term, and the layout is bound
    into the transcript through observe_shape (stage-2 widths, constraint
    counts and degrees).
  • Mᵢ = 0 — the one case where a chain constraint would leave its step
    unconstrained — occurs with probability ≤ N/|F_ext| over the challenges,
    since messages are committed before β, γ are sampled. This term is already
    in the union bound; previously the same event was a completeness failure
    (w·M = 1 unsatisfiable).
  • The accumulator gauge freedom is harmless: intermediate accumulators enter
    the protocol only as public values, and only differences are constrained.

Cost

Per circuit, stage 2 drops from 1 + L to ⌈L/k⌉ extension columns — at
k = 2, less than half the stage-2 FFT/Merkle/opening work — and the L
product-check constraints disappear (one chain constraint per group instead).
The quotient degree is unchanged wherever the grouped step degree does not
exceed the circuit's existing maximum. Downstream (Aiur, all messages
degree 2 at blowup 4 with k = 2), the modeled per-circuit FFT cost drops by
roughly 25–30%.

Breaking changes

  • Transcript/proof format: proofs from earlier versions no longer verify
    (different stage-2 layout and constraint set).
  • API: LookupAir's fields are no longer fully public (construct via
    new or from_parts); LookupValues::stage_2_traces takes group sizes.
  • Verifying-key codecs must serialize the lookup group size and rebuild airs
    with it (see from_parts).

Testing

  • All existing tests pass unchanged at their original blowups, on top of the
    new scheme.
  • New: end-to-end grouped proof at blowup 4 (grouped and group-of-one
    circuits in one system, stage-2 trace reduced to the accumulator column
    alone), a within-circuit remainder group, and a #[should_panic] test
    pinning the setup-time rejection message.
  • Exercised downstream by the Ix/Aiur suites: full proving pipeline
    (70 prove/verify cases), BLAKE3 and SHA-256 circuits, and the IxVM kernel
    checks, all green at k = 2, blowup 4.

@gabriel-barrett
gabriel-barrett marked this pull request as draft July 23, 2026 17:15
@gabriel-barrett
gabriel-barrett force-pushed the logup-optimization branch 4 times, most recently from 963f7ff to 8c9965e Compare July 28, 2026 11:58
Circuits are now data, not trait impls: frontend Expr/ExtExpr trees
compile to a flat, hash-consed, base-only node graph
(graph::ConstraintGraph) that the prover, verifier and witness generator
all evaluate with a dense forward sweep. Extension constraints are
coordinate-expanded at compile time (Karatsuba for D=2, schoolbook
otherwise), so the extension degree never appears in the compiled
artifact; constraint roots are canonicalized (const-zero dropped,
const-nonzero rejected at setup, sorted and deduplicated).

- expr.rs: frontend expression trees and the internal CircuitSpec
- graph.rs: interning compiler producing ConstraintGraph
- eval.rs: dense sweep evaluator shared by prover, verifier and the
  lookup witness (plus a recursive reference evaluator for tests)
- lookup.rs: logUp stage-2 constraints synthesized as ordinary ExtExpr
  data and compiled like user constraints; Lookup keeps its push/pull
  constructors
- system.rs: System over CircuitInputs (main width, preprocessed trace,
  constraints, lookups); stage-2 and public-input layout is derived at
  setup, not authored
- p3_adapter.rs: Plonky3-style authoring preserved. An AirBuilder
  implementation records Air::eval into Expr trees; LookupAir pairs an
  AIR with its lookups and converts into CircuitInputs; System::new
  accepts anything Into<CircuitInputs>. Existing AIR-based tests,
  examples and benches run unchanged except for import paths and the
  System type losing its AIR parameter.
- builder/ (symbolic expressions, constraint folders, check builder)
  deleted; prover/verifier keep the same protocol and transcript
Replace the committed-inverse logUp with a chained partial-accumulator
argument: the only stage-2 columns are the running accumulator and one
partial-accumulator column per lookup after the first — no message
inverse is ever committed (stage-2 width L·D instead of (1+L)·D).

Each lookup `(m, M)` contributes one chain step `M·(p − p_prev) = m`,
so `p − p_prev = m/M`, threaded from this row's accumulator column into
the next row's with a cyclic boundary correction `is_last·(next_acc −
acc)` — the step constraint is uniform across all rows, needing no
is_transition selector, and the accumulator's absolute level is
gauge-free (only its cyclic delta is pinned to the public claim).

The witness fills the accumulator with the direct fractional sum
`m·M⁻¹`: one batch inversion of the messages, per-lookup deltas computed
in parallel, and an additions-only prefix sum on the sequential path.
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.

1 participant