feat(ppsnark): Batch the Logup-GKR last layer with the Inner sumcheck batch - #502
Open
Sun-Jc wants to merge 5 commits into
Open
feat(ppsnark): Batch the Logup-GKR last layer with the Inner sumcheck batch#502Sun-Jc wants to merge 5 commits into
Sun-Jc wants to merge 5 commits into
Conversation
Replace the ppSNARK inverse-logup memory-check with a Logup-GKR fractional-sum argument, selectable by cargo feature. - Default: Logup-GKR. A fractional-sum GKR argument (src/spartan/logup_gkr/) folds the four row/col x table/access sub-instances into per-instance trees, with Horner lambda-batching (distinct powers per num/den) and MSB-first fold. - logup-no-gkr: the original inverse-logup memory-check (main's behaviour). The two paths are mutually exclusive and each compiles only its own code, so the GKR default drops the four inverse-oracle commitments and the six-route sumcheck. - Host bridge (src/spartan/mem_check_logup_gkr.rs) reconciles the claimed fingerprint columns against the GKR-reduced input claims (component-wise equality), runs the row/col balance check with a nonzero-denominator guard, and carries the seven reconcile columns into the inner batched sumcheck via a RerandomizeSumcheckInstance landing at r_inner_batched. - Verifier binds GKR depth to log N (rejects mismatched depth with NovaError instead of panicking) and uses exact fraction equality at the root gate and host reconcile to close the (0,0) projective-claim bypass. test_ivc_nontrivial_with_compression passes on both feature paths.
clippy 1.97 adds `useless_borrows_in_formatting` and `manual_clear`: - drop redundant `&` in write!/writeln! args (test_shape_cs pretty_print) - use Vec::clear() instead of truncate(0) (num gadget)
Reuse preceding-layer left final claims as the first sumcheck round's t(0), so the cached-delta pass only evaluates t(inf). The verifier and proof format are unchanged. At 20 variables, paired GKR prove benchmarks reduce median time by about 15 ms (~8%).
Build the outer sum-check third input uCz_E = u*Cz + E with a parallel iterator instead of a serial one. The map is memory-parallel and was needlessly serial: it shows a 4.2x-8.9x speedup on the construction itself (1.06ms -> 0.26ms at 2^16, 15.6ms -> 1.75ms at 2^20). The outer sum-check and all downstream logic are unchanged, so this is a zero-risk net win. End-to-end prover impact is ~0.1% (below the benchmark noise floor).
Replace the default GKR memory-check's independent opening-point reduction (the Logup-GKR argument to its own point, then a seven-column rerandomize sumcheck carrying the reconcile columns into the Inner sumcheck) with a single fused reduction: the GKR argument runs only to its input layer (the prefix), and its last layer shares the n-1 suffix rounds and the final MSB fold f with the Inner sumcheck's three relations, landing both on one shared point r_shared = f || s. This drops the seven-column rerandomize entirely and keeps a single PCS opening. Protocol: - Stage the Logup-GKR argument into prefix + last layer (shared per-round reduction body; standalone byte-equal to the prior monolithic loop). - A fresh beta (sampled after the four initial claims C_G/C_A/C_E/C_W are bound) batches the GKR last layer with the ABC / E / W Inner relations. n-1 shared suffix rounds send one fused cubic; the boundary uses h_sum = e_suffix - G_end (G_end reconstructed by the verifier from the input splits, never absorbed), then one shared fold f closes both. - Verifier checks the Inner endpoint h(f) == inner_expected and the GKR input-layer reconcile + balance separately (no RLC merge); component-wise exact fraction equality with four root-den != 0 guards; a forged GKR depth is rejected with Err. - n = 1 keeps the exact component-wise root-gate check. Proof size: -(3d+4) field elements vs the un-fused GKR path (drop the standalone inner-batched sumcheck's 3d coefficients + the 4 rerandomize columns). Prover time is neutral (memory-check is field-bound and dwarfed by the shared MSM stages); the fusion's win is proof size + memory. The fused driver borrows its memory-check columns: MemCheckWitness holds slices (build_input_layers only reads them to derive the fraction layers), so assembling it costs no per-column clone of the padded row/col columns. Two memory-check implementations remain mutually exclusive behind a cargo feature: default = fused Logup-GKR; logup-no-gkr = the original inverse-logup path. Both build and test.
Sun-Jc
force-pushed
the
feat/ppsnark/gkr-inner-batched
branch
from
July 26, 2026 07:16
274b1e5 to
c2a1c02
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.
Dependency: Should merge after #501
Summary
The default GKR memory-check currently runs the Logup-GKR fractional-sum argument to its own evaluation point, then a seven-column rerandomize sumcheck carries the reconcile columns into the Inner batched sumcheck's point — two independent
O(N)opening-point reductions of the same length. This PR fuses them: the GKR argument runs only to its input layer (the prefix), and its last layer shares then-1suffix rounds and the final MSB foldfwith the Inner sumcheck's three relations (ABC / E / W), landing both on one shared pointr_shared = f ∥ s. The seven-column rerandomize is deleted and a single PCS opening remains.beta(sampled after the four initial claimsC_G/C_A/C_E/C_Ware bound) batches the GKR last layer with the three Inner relations.n-1shared suffix rounds send one fused cubic; the boundary usesh_sum = e_suffix − G_end, withG_endreconstructed by the verifier from the input splits (never absorbed), then a single foldfcloses both protocols.h(f) == inner_expectedand the GKR input-layer reconcile + balance separately (no RLC merge); component-wise exact fraction equality with four root-denominator≠ 0guards; a forged GKR depth is rejected withErr.n = 1keeps the exact component-wise root gate.Two memory-check implementations remain mutually exclusive behind a cargo feature: default = fused Logup-GKR,
logup-no-gkr= the original inverse-logup path. Both build and test.What this is (and is not)
−(3d+4)field-element win over the un-fused GKR path — drop the standalone inner-batched sumcheck (3dcoefficients) plus the four rerandomize columns.smem_region1 127 → 1 022 ms). The gains are borrowing the memory-check columns instead of cloning six length-Nones, the Derive more traits. #7⊕Add support for using bellperson to generate R1CS. #8 fusion itself, and gettingts_row/ts_colfree from the GKR splits; part of that is given back by the fused path's serial inner-prep, which the un-fused path overlaps with its GKR. This is real but small: it is dwarfed by the shared MSM stages (commitL+ PCS ≈ 8 s ≈ 85% of e2e), so end-to-end wall time is a wash inside MSM run-to-run noise. The durable wins are proof size + prover memory (one fewer resident/committed column set), now with a small measurable field-time gain.O(d²), field-only (0 G) memory-check payload.Benchmark 1 — prover time, canonical 11-stage breakdown (3-way)
num_cons = 2²⁰(N_mem = 2²²), BN254 / HyperKZG,test-utils,DirectSNARK::proveonNonTrivialCircuit. 35 reps per config,PPS_STAGEprobe; every cell is that config's own 35-rep median (ms) — not pooled. ① and ③ are on this branch (thelogup-no-gkrfeature and the default respectively); ② was measured in afeat/ppsnark/gkrworktree, instrumented insidemem_check_logup_gkr::prove_stepand its monolithic GKR loop (split atj == 1) so every memory-check sub-stage is measured like-for-like against ③, not folded. Stages 0/1/2/9/10 are config-independent by construction (identicalz+SpMV, outer sumcheck,commit L_row/L_col, extra evals, single-polyEE::proveopen); their cross-config differences are pure MSM run-to-run noise (quantified in the third bullet). The memory-check region wall (smem_region, defined below the table) is the noise-free measure of the only real 3-way difference. Taxonomy fromjcbase/bench/2026-07-23-ppsnark-breakdown.md.logup-no-gkr)feat/ppsnark/gkr)z+ SpMV)LNcolumn clone)build_input_layers)val,masked_eq, slot build)j = d…2)j = 1)ts_row/ts_colre-opensmem_region)What
smem_regionis. It is the wall-clock of the whole memory-check + inner-sumcheck region ofprove: from the moment theL_row/L_colcommitments are absorbed (end of #2) to the start of the extra evals (#9). It therefore covers thec/gamma/rsqueezes, the entire memory-check (#3 for ①; #4a–#7 for ②③), the batched inner sumcheck (#8), ②'s extratsre-open (#8b), and the transcript glue between them. It is the honest apples-to-apples number because all three configs enter it with the same state and leave it having produced the same thing —r_inner_batchedplus every evaluation the batch opening needs. It is measured as one span, so unlike the per-stage medians it needs no summing assumption.Do not sum the sub-stage rows — compare at the region wall. The individual spans are disjoint and correctly measured (checked per-rep: in 35/35 reps the sub-stage sum is below the region wall, residual ≈ 47 ms, so there is no double counting), but a column sum is not comparable across ② and ③ for two reasons. (1) Concurrency asymmetry: ③ runs the inner-instance prep (#4c) serially for 77 ms, while ② runs the same O(N) work as
build_innerinside arayon::joinwith its GKR, so ②'s equivalent cost never appears as a serial span. (2) Unattributed region glue: ②'s spans leave ≈51 ms of the wall unmeasured (openings/proof-struct assembly, transcript, and freeing the extra 7-column rerandomize set), versus ≈−9 ms for ③. Summing the measured rows alone would put the gap at −121 ms; the honest, single-span measurement is −105 ms.Reading the table
batch_invert) and committing them (4 × length-NMSM, pure group work) — plus Add support for using bellperson to generate R1CS. #8 ≈ 0.38 s for the six-route inverse-logup sumcheck → wall ≈ 4.22 s. ②/③ commit nothing for the memory-check (Make traits public. #3 empty): every sub-stage is field arithmetic (0 G). So switching inverse-logup → GKR removes ~3 s of MSM and makes the prover field-bound there: wall 4.22 s → 1.13 / 1.02 s. This is the large, robust 3-way difference and the reason to prefer GKR.Nones (#4a — cross-checked two ways: ②'s clone span measures 52 ms, and re-cloning them inside ③ costs 52 ms); −37 ms net on the fusion proper (Derive more traits. #7 + Add support for using bellperson to generate R1CS. #8: ②257 + 225 = 482→ ③445 + 0, i.e. the last layer grows +188 ms as it takes on the Inner ABC/E/W relations, but the standalone inner sumcheck's 225 ms disappears); −15 ms readingts_row/ts_colfree from the folded GKR splits instead of re-opening them (#8b); −16 ms onbuild_input_layers(#4b). Against those, ③ pays +77 ms for its serial inner-prep (#4c), which ② overlaps with its GKR. The shared GKR work matches to within a millisecond (Input checks #5 110 vs 110, Add InputOutputMismatch error. #6 293 vs 292) — a useful sanity check that the two independent instrumentations agree.EE::prove= median 5 562 ms, min 4 941, max 6 423 — a 1 482 ms spread on identical work. The ③−②pcsmedian gap (283 ms) is 19 % of that pure single-poly jitter, and ②/③ swap places across passes (an earlier 35-rep pass put ②'spcsat 5 479 and itscommit Lat 2 356, this one at 5 681 / 2 648 — the same config moving ~300 ms).smem_regionstrips that noise and ranks ③ fastest.pcs(Add a test case that uses the bellperson adapter to generate R1CS #10) is identical across all three by construction —EE::proveopens one length-NRLC polynomial, independent of column count or memory-check; do not attribute any Add a test case that uses the bellperson adapter to generate R1CS #10 difference to fusion.Benchmark 2 — proof size (3-way, theoretical + measured)
Engine
Bn256EngineKZG+ HyperKZG (BN254);G= compressed G1 = 32 B,F= BN254 scalar = 32 B.c= outer rounds =log₂(num_cons)+1;d= inner / memory-check rounds =log₂(num_cons)+2. "Proof" = the ppSNARKRelaxedR1CSSNARKobject; actual =bincode(legacy) serialized length in bytes of a real prove output. Proof structure is byte-identical on this branch — the post-squash quality refactors are transcript-equivalent and touch no serialized type.Measured total bytes
Theoretical composition (crypto elements, then closed-form bytes incl. bincode framing)
(d+8) G + (3c+6d+19) F4 G + 4 F= 256 B(1.5·d(d−1) + 16d + 15) F,0 G52 d² + 476 d + 496(1.5·d(d−1) + 16d + 11) F,0 G52 d² + 476 d + 36852 d² + 708 d + 896−(3d + 4) F−(104 d + 136)(incl. framing)Theory-vs-measured (fused total, closed form
52 d² + 708 d + 896):Where the win comes from. The memory-check field itself shrinks by a flat 128 B (= 4 F) (drop the 7 rerandomize columns, net against the fused suffix's extra split/MSB data). The growing part of the win is the removed standalone
sc_inner_batched(dcubic rounds = 3d F), so the total delta is−(3d+4) F— reproducing the hand-derived bound exactly. Fusing shaves ~6–8 % off the GKR proof but does not close the gap to the flat4 G + 4 Finverse-logup baseline: the GKR memory-check is anO(d²), field-only payload that keeps config ③ 3.5×–4.8× larger than baseline, widening with size.Soundness & tests
crypto-security-reviewer) confirm no false-accept path: the fused MSB-fold boundary red-lines R1–R7 hold (G_endrecomputed from splits, endpoint/reconcile checked separately, splits+hbound beforef, component-wise exact equality + root-den≠ 0,betafresh after the four initial claims,h_sum = e_suffix − G_end, exactn=1root gate). Net added soundness error vs the un-fused path is the freshbeta's3/|F|.Err, not panic),reconcile_and_balancereject paths (mismatched column / unbalanced roots / zero root denominator), and a differential lock tying the fused last-layer gate algebra to the standalone GKR reconstruction.fmt/clippy -D warnings/doc --document-private-items;logup_gkr(28 tests) + fused module (8 tests);test_ivc_nontrivial_with_compressionon both the default (fused) andlogup-no-gkrfeatures.CI note
Both feature configs must build + test — the matrix needs
--features logup-no-gkr(and--features "logup-no-gkr,test-utils") alongside the default (which covers fused Logup-GKR).