logUp grouping - #526
Draft
gabriel-barrett wants to merge 4 commits into
Draft
Conversation
gabriel-barrett
force-pushed
the
logup-grouping
branch
2 times, most recently
from
July 31, 2026 16:35
03dfc7e to
db755fb
Compare
Bump multi-stark to the circuit-local lookup-grouping rev (07dc89d):
one chained accumulator step may consume k consecutive messages,
(prod_j m_j) * (acc' - acc) - sum_j mult_j * prod_{j'!=j} m_{j'} = 0
committing ceil(L/k) accumulator slots instead of L at the cost of a
higher logUp constraint degree. The group size is a free per-circuit
choice (it changes the constraint structure, not just the counts), so
the vk serializes it: a u8 after max_constraint_degree in each circuit
record, validated against MAX_LOOKUP_GROUP on decode, with the derived
stage_2_width and constraint_count now ceil(L/k)-based. Aiur keeps
lookup_group_size = 1 for every circuit for now, which reproduces the
previous protocol exactly except for the transcript shape.
Lean mirror:
- SystemDeserialize reads the group-size byte and derives ceil(L/k)
in-circuit by walking the lookups with a countdown (no division);
the observed shape is now 7 limbs per circuit (observe_shape gained
lookup_group_size after stage_2_width).
- The verifier's logup_steps_fold evaluates one constraint per lookup
GROUP, building each group's message product P and numerator R in a
single pass via R <- R*m + mult*P; P <- P*m; at k = 1 this is exactly
the old per-lookup chained step. The wrap group targets the next
row's slot 0 plus the is_last_row-scaled injection, unchanged.
The codec round-trip test covers a k = 2 circuit; the recursive
verifier e2e suite (prove -> verify in-circuit -> reject tampering)
passes against the new format, as do the aiur and multi-stark suites.
Codegen regenerated.
…step Lookup slots shared across branches superpose their arguments (sum_b sel_b * arg_b - sound because the selectors are mutually exclusive), which costs a degree: every argument becomes degree 2. A branchless function (a single selector, no matches) has exactly one branch per slot, so the weighting is pure waste: its arguments are now sent RAW (degree <= 1) via ConstraintState::gate, with the selector-gated multiplicity alone deciding whether the lookup counts. On padding rows the multiplicity is 0, which makes the (now nonzero, channel-bearing) message value irrelevant - the chained accumulator rides through unchanged, so the witness side needs no change. With degree-1 messages, two lookups fit in one chained-accumulator step at degree 3 (2 message degrees + 1 for the accumulator factor) - within the budget the selector-gated user constraints already pay for - so synthesis sets lookup_group_size = 2 on branchless functions with at least 2 lookups: ceil(L/2) committed stage-2 accumulators instead of L, plus a smaller vk (no selector-mul nodes per argument). Branching functions keep k = 1 (their degree-2 superposed arguments would blow the quotient budget), as do memory and gadget circuits. FunctionLayout.totalWidth mirrors the grouping rule so the circuit statistics stay honest. The recursive-verifier e2e now proves through a branchless entrypoint (store -> load -> call factorial: 4 lookups, k = 2), so the in-circuit grouped logUp fold verifies a real grouped proof and still rejects tampering; the aiur prove/verify and multi-stark suites pass.
The branchless raw-argument + k = 2 grouping change narrows every branchless function circuit's stage-2 width (ceil(L/2) accumulators instead of L), which the statistics reflect via FunctionLayout.totalWidth. Re-pin all 66 kernel-check FFT-cost constants to the measured values - a 15-19% reduction across the board (e.g. Nat.add_comm 48,989,755 -> 40,570,358; Vector.append 2,210,433,232 -> 1,877,570,289). ixvm suite fully green, incl. codegen/bytecode parity; aiur-hashes (blake3 + sha256 prove/verify, grouping-heavy branchless circuits) and rbtree-map suites pass.
The Bytes1/Bytes2 lookup arguments are preprocessed columns and their multiplicities main columns - all degree 1, same shape as branchless function lookups - so they group 2 per chained-accumulator step at degree 3. Stage-2 accumulators: Bytes1 3 -> 2, Bytes2 10 -> 5 at its fixed height of 65536 rows, where the width saving actually pays. The kernel FFT-cost pins are untouched: the statistics cover function and memory circuits only. aiur, aiur-hashes (blake3/sha256 drive the byte chips hard), ixvm, multi-stark, and recursive-verifier suites all pass.
gabriel-barrett
force-pushed
the
logup-grouping
branch
from
July 31, 2026 17:10
c398d91 to
11cb3c9
Compare
Member
Author
|
!benchmark aiur fresh |
Member
Author
|
!benchmark aiur-recursive fresh |
|
| constant | prove-time (main) | prove-time (PR) | Δ% | throughput (const/s) (main) | throughput (const/s) (PR) | Δ% | peak-ram (main) | peak-ram (PR) | Δ% | execute-time (main) | execute-time (PR) | Δ% | verify-time (main) | verify-time (PR) | Δ% | proof-size (main) | proof-size (PR) | Δ% | fft-cost (main) | fft-cost (PR) | Δ% |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Array.extract_append |
44.671 s | 44.342 s | -0.7% | 38.190 | 38.470 | +0.7% | 92.21 GiB | 87.96 GiB | -4.6% 🟢 | 10.565 s | 10.759 s | +1.8% | 167.0 ms | 151.0 ms | -9.6% (1.11× faster) 🟢 | 24.90 MiB | 24.07 MiB | -3.4% 🟢 | 34.11B | 32.44B | -4.9% (1.05× fewer) 🟢 |
ByteArray.utf8DecodeChar?_utf8EncodeChar_append |
44.086 s | 43.066 s | -2.3% | 65.550 | 67.110 | +2.4% | 98.56 GiB | 90.22 GiB | -8.5% (1.09× smaller) 🟢 | 9.296 s | 9.268 s | -0.3% | 151.9 ms | 147.4 ms | -3.0% 🟢 | 24.98 MiB | 24.15 MiB | -3.4% 🟢 | 36.37B | 32.68B | -10.1% (1.11× fewer) 🟢 |
Char.ofOrdinal_le_of_le |
34.658 s | 34.469 s | -0.5% | 82.580 | 83.030 | +0.5% | 76.86 GiB | 72.50 GiB | -5.7% (1.06× smaller) 🟢 | 6.759 s | 6.866 s | +1.6% | 154.0 ms | 161.3 ms | +4.7% |
24.93 MiB | 24.09 MiB | -3.3% 🟢 | 27.80B | 25.07B | -9.8% (1.11× fewer) 🟢 |
Vector.extract_append._proof_2 |
25.716 s | 25.396 s | -1.2% | 56.030 | 56.740 | +1.3% | 50.85 GiB | 48.66 GiB | -4.3% 🟢 | 5.846 s | 5.668 s | -3.0% 🟢 | 152.6 ms | 145.5 ms | -4.7% 🟢 | 24.58 MiB | 23.75 MiB | -3.4% 🟢 | 20.00B | 18.81B | -6.0% (1.06× fewer) 🟢 |
_private.Init.Data.Range.Polymorphic.SInt.0.Int64.instRxcHasSize_eq |
21.412 s | 20.853 s | -2.6% | 92.150 | 94.610 | +2.7% | 48.46 GiB | 44.32 GiB | -8.5% (1.09× smaller) 🟢 | 3.459 s | 3.502 s | +1.3% | 161.2 ms | 161.0 ms | -0.1% | 24.75 MiB | 23.92 MiB | -3.4% 🟢 | 15.01B | 13.38B | -10.9% (1.12× fewer) 🟢 |
String.split |
20.460 s | 19.646 s | -4.0% 🟢 | 95.260 | 99.210 | +4.1% 🟢 | 46.56 GiB | 42.37 GiB | -9.0% (1.10× smaller) 🟢 | 3.147 s | 3.196 s | +1.6% | 155.9 ms | 151.3 ms | -3.0% | 24.94 MiB | 24.10 MiB | -3.3% 🟢 | 13.59B | 12.15B | -10.6% (1.12× fewer) 🟢 |
List.mergeSort |
14.338 s | 13.983 s | -2.5% | 111.450 | 114.280 | +2.5% | 30.69 GiB | 28.56 GiB | -7.0% (1.07× smaller) 🟢 | 2.295 s | 2.278 s | -0.7% | 152.7 ms | 156.7 ms | +2.6% | 24.80 MiB | 23.97 MiB | -3.4% 🟢 | 10.00B | 8.88B | -11.3% (1.13× fewer) 🟢 |
Vector.append |
4.828 s | 4.765 s | -1.3% | 117.230 | 118.780 | +1.3% | 8.20 GiB | 8.01 GiB | -2.3% | 606.8 ms | 607.0 ms | +0.0% | 143.1 ms | 140.7 ms | -1.7% | 23.45 MiB | 22.66 MiB | -3.3% 🟢 | 2.12B | 1.88B | -11.4% (1.13× fewer) 🟢 |
Nat.gcd_comm |
4.080 s | 3.949 s | -3.2% 🟢 | 101.710 | 105.100 | +3.3% 🟢 | 7.53 GiB | 7.49 GiB | -0.5% | 478.7 ms | 470.5 ms | -1.7% | 153.3 ms | 142.0 ms | -7.4% (1.08× faster) 🟢 | 23.07 MiB | 22.31 MiB | -3.3% 🟢 | 1.50B | 1.30B | -13.3% (1.15× fewer) 🟢 |
String.append |
2.978 s | 2.970 s | -0.3% | 118.540 | 118.870 | +0.3% | 5.28 GiB | 6.20 GiB | +17.4% (1.17× larger) |
356.3 ms | 353.7 ms | -0.7% | 142.8 ms | 133.1 ms | -6.8% (1.07× faster) 🟢 | 22.40 MiB | 21.64 MiB | -3.4% 🟢 | 835.80M | 728.21M | -12.9% (1.15× fewer) 🟢 |
Int.gcd |
2.485 s | 2.472 s | -0.5% | 92.140 | 92.640 | +0.5% | 5.80 GiB | 5.23 GiB | -9.7% (1.11× smaller) 🟢 | 294.9 ms | 298.6 ms | +1.3% | 137.7 ms | 128.3 ms | -6.8% (1.07× faster) 🟢 | 22.04 MiB | 21.29 MiB | -3.4% 🟢 | 518.71M | 450.66M | -13.1% (1.15× fewer) 🟢 |
Nat.sub_le_of_le_add |
2.367 s | 2.320 s | -2.0% | 80.270 | 81.890 | +2.0% | 5.82 GiB | 4.91 GiB | -15.5% (1.18× smaller) 🟢 | 301.5 ms | 282.3 ms | -6.3% (1.07× faster) 🟢 | 143.3 ms | 131.4 ms | -8.3% (1.09× faster) 🟢 | 22.49 MiB | 21.75 MiB | -3.3% 🟢 | 434.24M | 377.91M | -13.0% (1.15× fewer) 🟢 |
Nat.add_comm |
1.298 s | 1.303 s | +0.4% | 39.280 | 39.130 | -0.4% | 4.41 GiB | 4.67 GiB | +5.8% (1.06× larger) |
199.9 ms | 197.8 ms | -1.1% | 123.4 ms | 125.2 ms | +1.4% | 21.01 MiB | 20.27 MiB | -3.5% 🟢 | 47.09M | 40.57M | -13.8% (1.16× fewer) 🟢 |
|
| proof | recursive-prove-time (main) | recursive-prove-time (PR) | Δ% | recursive-peak-ram (main) | recursive-peak-ram (PR) | Δ% | recursive-proof-size (main) | recursive-proof-size (PR) | Δ% | recursive-verify-time (main) | recursive-verify-time (PR) | Δ% | recursive-execute-time (main) | recursive-execute-time (PR) | Δ% | recursive-fft-cost (main) | recursive-fft-cost (PR) | Δ% | prove-time (main) | prove-time (PR) | Δ% | proof-size (main) | proof-size (PR) | Δ% | verify-time (main) | verify-time (PR) | Δ% | peak-ram (main) | peak-ram (PR) | Δ% |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
factorial-q100-b2 |
4.231 s | 3.771 s | -10.9% (1.12× faster) 🟢 | 14.15 GiB | 12.25 GiB | -13.5% (1.16× smaller) 🟢 | 10.60 MiB | 10.21 MiB | -3.7% 🟢 | 65.6 ms | 66.0 ms | +0.6% | 375.4 ms | 359.3 ms | -4.3% 🟢 | 2.94B | 2.26B | -23.0% (1.30× fewer) 🟢 | 82.3 ms | 89.7 ms | +9.0% (1.09× slower) |
838.17 KiB | 831.61 KiB | -0.8% | 6.2 ms | 6.4 ms | +1.7% | 352.42 MiB | 340.09 MiB | -3.5% 🟢 |
square-q100-b1 |
2.131 s | 2.141 s | +0.5% | 5.10 GiB | 5.58 GiB | +9.4% (1.09× larger) |
10.46 MiB | 10.06 MiB | -3.8% 🟢 | 65.2 ms | 63.4 ms | -2.7% | 310.8 ms | 329.8 ms | +6.1% (1.06× slower) |
2.43B | 1.93B | -20.2% (1.25× fewer) 🟢 | 64.0 ms | 73.8 ms | +15.4% (1.15× slower) |
771.61 KiB | 765.05 KiB | -0.9% | 5.6 ms | 5.8 ms | +4.0% |
262.80 MiB | 267.82 MiB | +1.9% |
gabriel-barrett
force-pushed
the
logup-grouping
branch
from
July 31, 2026 18:29
7a1ac0e to
11cb3c9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adopt multi-stark logUp grouping; raw lookup arguments on branchless functions
Adopts multi-stark's circuit-local logUp message grouping (
07dc89d, branchlogup-grouping) and puts it to work in aiur. Sits on top oflogup-optimization.LogUp grouping (multi-stark side, recap)
One chained-accumulator step may consume
kconsecutive messages:which forces
acc′ = acc + Σ_j mult_j/m_jexactly askungrouped steps would, but commits⌈L/k⌉accumulator slots instead ofL— at the cost of raising the logUp constraint degree to aboutΣ_j deg(m_j) + 1. The group size is circuit-local (CircuitInputs::lookup_group_size, default 1) because the degree headroom depends on each circuit's message degrees; it is bounded byMAX_LOOKUP_GROUP = 8and observed into the transcript shape.vk codec
The group size is a free per-circuit choice (it changes the constraint structure, not just counts), so it can't be derived: each circuit record now carries a
u8 lookup_group_sizeaftermax_constraint_degree, validated againstMAX_LOOKUP_GROUPon decode. The derived quantities becomestage_2_width = max(⌈L/k⌉, 1)·Dandconstraint_count = zeros + ⌈L/k⌉·D. The round-trip test coversa
k = 2circuit.Lean recursive verifier
SystemDeserialize.leanreads the group-size byte and derives⌈L/k⌉in-circuit by walking the lookups with a countdown (no in-circuit division). The observed shape is now 7 limbs per circuit —observe_shapegainedlookup_group_sizeafterstage_2_width.Verifier.lean'slogup_steps_foldevaluates one constraint per lookup group, building each group's message productPand numeratorRin a single pass viaR ← R·m + mult·P; P ← P·m. Atk = 1this reduces to exactly the previous per-lookup chained step. The wrap group still targets the next row's slot 0 plus theis_last_row-scaled injection.Branchless functions: raw arguments +
k = 2Lookup slots shared across branches superpose their arguments (
Σ_b sel_b·arg_b— sound because selectors are mutually exclusive), which costs a degree: every argument becomes degree 2. A branchless function (single selector, no matches) has exactly one branch per slot, so the weighting is pure waste. Its arguments are now sent raw (degree ≤ 1) viaConstraintState::gate; theselector-gated multiplicity alone decides whether the lookup counts.
This is witness-compatible with no witness-side change: the trace builder already records raw concrete arguments, active rows have
sel = 1so the expressions agree, and padding rows have multiplicity 0, which makes the message value irrelevant — the chained accumulator rides through unchanged.With degree-1 messages, two lookups fit in one chained step at degree 3 — within the budget the selector-gated user constraints already pay for — so synthesis sets
lookup_group_size = 2on branchless functions with ≥ 2 lookups: half the stage-2 accumulator columns, plus a smaller vk (no selector-mul nodes per argument). Branching functions, memories, and gadgets keepk = 1.Impact
Most kernel circuits are branchless, so the FFT-cost proxy drops 15–19% across the board; all 66 kernel-check pins re-measured, e.g.:
Nat.add_commNat.decLeVector.appendFunctionLayout.totalWidthmirrors the grouping rule so the circuit statistics stay honest.Testing
cargo test -p aiur(incl. vk round-trip with a grouped circuit) — passlake test -- --ignored multi-stark/recursive-verifier— pass; the e2e now proves through a branchless entrypoint (fact_entry(n) = factorial(load(store(n))), 4 lookups,k = 2), so the in-circuit grouped logUp fold verifies a real grouped proof and still rejects tamperinglake test -- --ignored ixvm— all 66 FFT pins green, codegen/bytecode parity greenlake test -- --ignored aiur/aiur-hashes(blake3 + sha256, grouping-heavy) /rbtree-map— pass