Skip to content

logUp grouping - #70

Open
gabriel-barrett wants to merge 1 commit into
mainfrom
logup-grouping
Open

logUp grouping#70
gabriel-barrett wants to merge 1 commit into
mainfrom
logup-grouping

Conversation

@gabriel-barrett

Copy link
Copy Markdown
Member

logUp: circuit-local message grouping

Adds CircuitInputs::lookup_group_size (default 1): one chained
accumulator step may consume k consecutive messages instead of one,
committing ⌈L/k⌉ stage-2 accumulator slots instead of L — at the
cost of a higher logUp constraint degree. The option is per circuit,
not global: whether grouping fits depends on each circuit's message
degrees and its remaining degree budget.

How it works

The ungrouped chained step forces acc' = acc + mult/m via
m·(acc' − acc) − mult = 0. For a group m_0, …, m_{s−1} the
sum-of-fractions identity is multiplied through by the group's message
product:

(Π_j m_j) · (acc' − acc)  −  Σ_j mult_j · Π_{j'≠j} m_{j'}  =  0

which forces acc' = acc + Σ_j mult_j/m_j exactly as s ungrouped
steps would, but with one committed accumulator slot instead of
s. Lookups are chunked in order; the last group may be smaller. The
final group's step targets the next row's slot 0 plus the boundary
injection is_last_row·Δ, so the cyclic-telescoping argument is
unchanged from the chained scheme.

The degree per group is roughly Σ_j deg(m_j) + 1 (precisely
max(Σ deg(m_j) + 1, max_j [deg(mult_j) + Σ_{j'≠j} deg(m_{j'})]),
computed analytically by logup_max_degree). E.g. two degree-1
messages group at degree 3; degree-2 messages usually can't afford
grouping — hence the circuit-local dial.

Implementation

  • synthesize_lookups (the pinned executable spec) and
    logup_constraint_values (the direct evaluator used by prover and
    verifier) chunk lookups by the group size. Group products use
    prefix/suffix arrays so each Π_{j'≠j} costs one multiplication;
    scratch stays on the stack, bounded by MAX_LOOKUP_GROUP = 8.
  • The singleton-group path is special-cased to the exact
    pre-grouping arithmetic — the default k = 1 pays nothing for the
    feature.
  • stage_2_traces pushes one accumulator per group per row; still a
    single batch inversion plus one linear pass, one multiply-add per
    (row, lookup).
  • The group size is observed into the transcript shape
    (observe_shape): it changes the constraint structure, which
    counts and degrees alone don't pin down.
  • Setup asserts lookup_group_size ≤ MAX_LOOKUP_GROUP and the usual
    quotient-degree bound fails loudly for a too-aggressive grouping.

Add `CircuitInputs::lookup_group_size` (default 1): one chained
accumulator step may consume k messages at once,

  (prod_j m_j) * (acc' - acc) - sum_j mult_j * prod_{j'!=j} m_{j'} = 0

which forces acc' = acc + sum_j mult_j/m_j exactly as k ungrouped steps
would, but with ceil(L/k) committed accumulator slots instead of L - at
the cost of raising the logUp constraint degree to about
sum_j deg(m_j) + 1. The option is per circuit because the degree budget
depends on each circuit's message degrees; it is bounded by
MAX_LOOKUP_GROUP (8) so the evaluator's group scratch stays on the
stack, and observed into the transcript shape since it changes the
constraint structure.

- synthesize_lookups (the pinned spec) and logup_constraint_values (the
  direct evaluator) chunk lookups by the group size; group products use
  prefix/suffix arrays so each prod_{j'!=j} costs one multiplication.
  The singleton-group path is special-cased to the exact pre-grouping
  arithmetic, so the default k = 1 costs what it did before.
- stage_2_traces pushes one accumulator per GROUP per row (still batch
  inversion + a single linear pass, one multiply-add per lookup).
- logup_max_degree computes the grouped degree analytically per chunk:
  max(sum deg(m_j) + 1, max_j (deg(mult_j) + sum_{j'!=j} deg(m_{j'}))).
- The pin test runs group sizes 1, 2, 3 over 3 lookups (covering an
  uneven tail); the u32_add e2e test groups its 13 degree-1 messages in
  pairs while the byte table stays ungrouped, covering mixed group
  sizes in one system.

A/B bench (lookup-heavy circuit, 16 lookups with degree-2 messages,
2^14 rows, vs a6f5441): default k = 1 at parity (quotient constraint
eval ~19ms, stage-2 build ~9-14ms on both); k = 2 halves the stage-2
accumulator columns and doubles quotient eval (~38ms) because degree
3 -> 5 doubles the quotient domain - the intended per-circuit tradeoff.
@gabriel-barrett
gabriel-barrett marked this pull request as ready for review July 31, 2026 16:39
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