Skip to content

feat(ppsnark): Batch the Logup-GKR last layer with the Inner sumcheck batch#502

Open
Sun-Jc wants to merge 5 commits into
microsoft:mainfrom
Sun-Jc:feat/ppsnark/gkr-inner-batched
Open

feat(ppsnark): Batch the Logup-GKR last layer with the Inner sumcheck batch#502
Sun-Jc wants to merge 5 commits into
microsoft:mainfrom
Sun-Jc:feat/ppsnark/gkr-inner-batched

Conversation

@Sun-Jc

@Sun-Jc Sun-Jc commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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 the n-1 suffix rounds and the final MSB fold f with the Inner sumcheck's three relations (ABC / E / W), landing both on one shared point r_shared = f ∥ s. The seven-column rerandomize is deleted and a single PCS opening remains.

  • 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 three Inner relations.
  • n-1 shared suffix rounds send one fused cubic; the boundary uses h_sum = e_suffix − G_end, with G_end reconstructed by the verifier from the input splits (never absorbed), then a single fold f closes both protocols.
  • The 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-denominator ≠ 0 guards; a forged GKR depth is rejected with Err. n = 1 keeps 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)

  • Proof size: a guaranteed −(3d+4) field-element win over the un-fused GKR path — drop the standalone inner-batched sumcheck (3d coefficients) plus the four rerandomize columns.
  • Prover time: neutral vs un-fused GKR. The memory-check is field-bound and dwarfed by the shared MSM stages (commit L + PCS open); fusing the field band does not move end-to-end wall time. The win is proof size + prover memory (one fewer resident/committed column set).
  • Choosing GKR over inverse-logup at all is a size↔time trade: GKR removes ~3.5 s of inverse-oracle MSM from the prover, at the cost of a larger, 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::prove on NonTrivialCircuit. 20 reps per config, PPS_STAGE probe. ② (un-fused) was measured in a feat/ppsnark/gkr worktree; its GKR runs inside a rayon::join, so the probe records sub-stages through a global-Mutex span buffer to survive work-stealing. Stages 0/1/2/9/10 are config-independent (identical z+SpMV, outer sumcheck, commit L_row/L_col, extra evals, and the single-poly EE::prove open), so they carry one pooled value across the three columns — their per-config spread is pure run-to-run noise (the PCS within-config spread alone is 4.8–6.4 s, dwarfing the mean gaps). The memory-check band (#3#8) is the only real difference. This is the taxonomy from jcbase/bench/2026-07-23-ppsnark-breakdown.md; the sub-stage cell values are each config's own 20-rep mean (ms).

# stage ① baseline (logup-no-gkr) ② un-fused GKR (feat/ppsnark/gkr) ③ fused (this PR) cost
0 prepare (z + SpMV) 99 99 99 field
1 outer sum-check 81 81 81 field
2 commit L 2 470 2 470 2 470 MSM
3 prepare mem-check (build + commit 4 inv oracles) 3 969 field + MSM
4 gkr prepare (build_input_layers) 130 128 field
5 gkr build tree 112 123 field
6 gkr first layers (j = d…2) 299 301 field
7 gkr last layer (j = 1) 262 473 (fused ⊕ inner) field
8 inner-sc-batch 392 230 0 (merged into #7) field
9 evals 37 37 37 field
10 pcs open 5 685 5 685 5 685 MSM
mem-check band (#3#8) 4 361 1 033 1 025
total (pooled shared 8 372 + band) 12 733 9 405 9 397

* Both paths also do the inner-instance prep (val = val_A + c·val_B + c²·val_C, masked_eq, the sumcheck-slot build), which is not counted in the band above: ② overlaps its build_inner with the GKR via rayon::join (hidden in wall-clock), while ③ currently runs it sequentially before the GKR (~147 ms). It is the same work in both — a scheduling difference, not a fusion cost — so the measured top-level memcheck region is a touch higher than the band sum (② ~1 154, ③ ~1 189 ms). ③ could overlap it too.

Reading the table


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 ppSNARK RelaxedR1CSSNARK object; 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

log₂ num_cons (c, d) ① baseline ② un-fused GKR ③ fused (this PR) ③ − ② ③ / ①
10 (11,12) 4 824 18 264 16 880 −1 384 (−7.6%) 3.5×
12 (13,14) 5 496 22 592 21 000 −1 592 (−7.0%) 3.8×
14 (15,16) 6 168 27 336 25 536 −1 800 (−6.6%) 4.1×
16 (17,18) 6 840 32 496 30 488 −2 008 (−6.2%) 4.5×
18 (19,20) 7 512 38 072 35 856 −2 216 (−5.8%) 4.8×

Theoretical composition (crypto elements, then closed-form bytes incl. bincode framing)

config crypto elements closed-form bytes matches measured?
① baseline total (d+8) G + (3c+6d+19) F memcheck flat 4 G + 4 F = 256 B yes
② un-fused GKR memcheck (1.5·d(d−1) + 16d + 15) F, 0 G 52 d² + 476 d + 496 yes (every row)
③ fused GKR memcheck (1.5·d(d−1) + 16d + 11) F, 0 G 52 d² + 476 d + 368 yes (every row)
③ fused total 52 d² + 708 d + 896 yes (every row)
Δ (③ − ②) −(3d + 4) F −(104 d + 136) (incl. framing) yes

Theory-vs-measured (fused total, closed form 52 d² + 708 d + 896):

log₂ num_cons (c, d) theoretical measured
10 (11,12) 16 880 16 880
14 (15,16) 25 536 25 536
18 (19,20) 35 856 35 856

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 (d cubic 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 flat 4 G + 4 F inverse-logup baseline: the GKR memory-check is an O(d²), field-only payload that keeps config ③ 3.5×–4.8× larger than baseline, widening with size.


Soundness & tests

  • Two independent audits (manual protocol replay + crypto-security-reviewer) confirm no false-accept path: the fused MSB-fold boundary red-lines R1–R7 hold (G_end recomputed from splits, endpoint/reconcile checked separately, splits+h bound before f, component-wise exact equality + root-den ≠ 0, beta fresh after the four initial claims, h_sum = e_suffix − G_end, exact n=1 root gate). Net added soundness error vs the un-fused path is the fresh beta's 3/|F|.
  • New negative tests: forged-GKR-depth rejection (Err, not panic), reconcile_and_balance reject paths (mismatched column / unbalanced roots / zero root denominator), and a differential lock tying the fused last-layer gate algebra to the standalone GKR reconstruction.
  • Gates green: fmt / clippy -D warnings / doc --document-private-items; logup_gkr (28 tests) + fused module (8 tests); test_ivc_nontrivial_with_compression on both the default (fused) and logup-no-gkr features.

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).

Sun-Jc added 5 commits July 14, 2026 13:33
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.

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.
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