Skip to content

perf: parallelize LDE / coset-FFT / Merkle pipeline#7

Merged
ThomasPiellard merged 1 commit into
mainfrom
perf/parallel-lde-merkle-fri
May 28, 2026
Merged

perf: parallelize LDE / coset-FFT / Merkle pipeline#7
ThomasPiellard merged 1 commit into
mainfrom
perf/parallel-lde-merkle-fri

Conversation

@gbotrel

@gbotrel gbotrel commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Fans the prover hot path out across goroutines. Every previously-serial loop over polynomials, leaves, fold layers and per-element divisions is now run via parallel.Execute (or ExecuteWithThreshold for tiny inputs). Loops that carried a serial multiplicative accumulator (xInv = g^i, acc = invFrGen^k) are split into chunks whose starting value is precomputed via binary exponentiation (poly.PowUint64) so chunks run independently.

Measured impact

Synth bench, 32-core box, KoalaBear + Poseidon2 + blowup=4 + 4 queries:

shape trace cells before after speedup CPU util (user/wall) before → after
tall (2²⁰ rows × 24 cols) 25.2 M 13.85 s 2.51 s 5.5× 1.14× → 4.1×
wide (2¹⁴ rows × 1536 cols) 25.2 M 1.38 s 0.70 s 2.0× 1.77× → 2.6×

Phase-level for tall:

phase before after speedup
execute-steps 2.50 s 0.23 s 10.7×
compute-air-quotients 2.86 s 0.55 s 5.2×
evaluations-at-zeta 0.53 s 0.52 s unchanged (still serial — next step)
deep-quotient+fri-commit 7.75 s 0.99 s 7.8×
fri-query-open 0.10 s 0.10 s unchanged (negligible)

Verifier wall is unchanged (~1.4 ms tall / 2.9 ms wide). Proof size unchanged.

Where each change lives

  • internal/commitment/ — per-polynomial Encode / EncodeExt loops in RSCommit.Commit run in parallel; exposed HashLeavesParallel so FRI can use the same batched (rate-16 Poseidon2) leaf-hashing path.
  • internal/merkle/tree.goTree.Build and BuildNodes hash each tree level in parallel above a width threshold (parallelLevelThreshold = 64); level-to-level walk stays serial (data dependency).
  • internal/fri/fri.gobuildTree{Base,Ext} now use the batched parallel leaf hasher instead of a serial per-leaf HashLeaf; foldLayer{Base,Ext} are chunked with per-chunk xInv = GeneratorInv^chunkStart seeded via poly.PowUint64.
  • internal/poly/compute_quotient.goComputeQuotient and ComputeQuotientMixed fan out every per-polynomial FFT / FFTInverse pass and the final per-element divide; CosetLagrangeNormalToCanonical{,Ext}WithCache parallelize the acc · invFrGen^k loop via chunked invFrGen seeding.
  • internal/parallel/parallel.go — small ExecuteWithThreshold(n, threshold, work) helper to replace the repeated "if n ≥ threshold fan out, else serial" pattern.
  • internal/poly/utils.goPowUint64(base, exp) binary exponentiation used to seed parallel chunks.
  • prover/prover.go — new WithPhaseCallback(fn) option; the prover fires fn(name, dur) after each major phase (execute-steps, compute-air-quotients, evaluations-at-zeta, deep-quotient+fri-commit, fri-query-open).
  • bench/synth/main.go — new minimal reproducer that proves a parameterized deg-2 row-local synth AIR and reports per-phase timings + proof size + verify time. Used to validate this change and benchmark vs Plonky3.

Test plan

  • go test ./...
  • go test -race ./internal/poly/... ./internal/fri/... ./internal/commitment/... ./internal/merkle/... ./prover/...
  • go vet ./... — only pre-existing lock-copy warnings on proverRuntime (unrelated to this PR)
  • go run ./bench/synth -log2-rows 20 -repetitions 8 verifies and prints the new phase breakdown
  • go run ./bench/synth -log2-rows 14 -repetitions 512 same
  • PLONK aggregation bench (go run ./bench) still runs end-to-end and the produced proof verifies

Notes / follow-ups

  • Two remaining hot phases are now clearly visible in the synth bench and still single-threaded — out of scope here:
    • evaluations-at-zeta (22% of tall, 57% of wide): EvaluateAtExt per column inside ComputeEvaluationsAtZeta.
    • DEEP quotient assembly inside ComputeDeepQuotient: O(W·N) sequential ext-field work.

🤖 Generated with Claude Code


Note

Medium Risk
Touches core proving (commitments, FRI, quotients) but keeps the same math and verifier behavior; risk is mainly correctness/regressions under concurrency rather than security or API breaks.

Overview
Parallelizes the prover’s LDE / coset-FFT / Merkle / FRI / quotient pipeline so independent work runs via parallel.Execute and ExecuteWithThreshold, with chunk seeding (poly.PowUint64) where loops had serial multiplicative state (xInv, invFrGen^k).

Commitment & Merkle: per-polynomial Reed–Solomon Encode/EncodeExt and leaf hashing fan out; HashLeavesParallel is shared by RS commit and FRI tree builds. Merkle Build hashes wide internal levels in parallel above a width threshold.

FRI & poly: foldLayerBase/Ext and quotient/coset paths parallelize FFT passes and final divides; coset-to-canonical scaling is parallelized the same way.

Observability & bench: prover.WithPhaseCallback reports wall time per major prove phase; new bench/synth proves a configurable deg-2 synthetic AIR and prints phase breakdown, proof size, and verify time (for apples-to-apples benchmarking).

Verifier time and proof format are unchanged; gains are prover wall time and CPU utilization on multi-core hosts.

Reviewed by Cursor Bugbot for commit 41d6eb3. Bugbot is set up for automated code reviews on this repo. Configure here.

Fans the prover hot path out across goroutines: every previously-serial
loop over polynomials, leaves, fold layers and per-element divisions is
now run via parallel.Execute (or ExecuteWithThreshold for tiny inputs).

Loops that carried a serial multiplicative accumulator are split into
chunks whose starting value is precomputed via binary exponentiation
(poly.PowUint64) so chunks can run independently.

Touched:
- internal/commitment: per-poly Encode loops in RSCommit.Commit run in
  parallel; expose HashLeavesParallel for reuse.
- internal/merkle: Tree.Build hashes each level in parallel above a
  width threshold; level-to-level walk remains serial.
- internal/fri: buildTree{Base,Ext} use the batched parallel leaf hasher;
  foldLayer{Base,Ext} chunked with per-chunk xInv seeding.
- internal/poly: ComputeQuotient and ComputeQuotientMixed fan out the
  per-poly FFT passes and the final per-element divide;
  CosetLagrangeNormalToCanonical{Ext}WithCache parallelize the
  acc-multiply loop via chunked invFrGen seeding.
- internal/parallel: ExecuteWithThreshold helper.
- prover: WithPhaseCallback option to report wall time per major phase
  ("execute-steps", "compute-air-quotients", ...).
- bench/synth: new minimal reproducer that proves a parameterized
  deg-2 row-local synth AIR and reports per-phase timings + proof size
  + verify time. Used to validate this change and benchmark vs Plonky3.

Measured on a 32-core machine, KoalaBear + Poseidon2 + blowup=4 + 4 queries:

  shape (cells)              before    after    speedup
  tall  (2^20 rows × 24)     13.85 s   2.51 s   5.5×
  wide  (2^14 rows × 1536)    1.38 s   0.70 s   2.0×

CPU utilization (user/wall) goes from 1.14× / 1.77× to 4.1× / 2.6×.
All existing tests stay green (incl. -race on the hot packages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 27, 2026 16:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR parallelizes major parts of the prover hot path (quotient computation, coset FFTs, Merkle building, and FRI folding/commitment) using the internal parallel helper, including chunk seeding for loops that previously had serial multiplicative dependencies. It also adds optional per-phase timing callbacks in the prover and introduces a new synthetic benchmark to measure end-to-end and per-phase performance.

Changes:

  • Parallelize quotient computation, coset conversions, FRI folding, leaf hashing, and Merkle internal-node hashing using parallel.Execute / ExecuteWithThreshold.
  • Add poly.PowUint64 for fast exponentiation used to seed per-chunk accumulators in parallel loops.
  • Add prover.WithPhaseCallback and a new bench/synth command to report per-phase timings and proof/verify stats.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
prover/prover.go Adds WithPhaseCallback and phase duration reporting around major proving phases.
internal/poly/utils.go Introduces PowUint64 (binary exponentiation) to seed parallel chunk accumulators.
internal/poly/compute_quotient.go Parallelizes FFT/coset passes and final divides; uses chunk seeding for scaling loops.
internal/parallel/parallel.go Adds ExecuteWithThreshold to avoid parallel overhead on small workloads.
internal/merkle/tree.go Parallelizes hashing within Merkle tree levels above a threshold.
internal/fri/fri.go Uses shared parallel leaf hashing and parallelizes fold layers with chunk-seeded xInv.
internal/commitment/commitment.go Parallelizes per-polynomial encoding and exposes shared HashLeavesParallel.
bench/synth/main.go Adds a synthetic AIR benchmark that reports prove/verify totals, phase breakdown, and proof size.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread bench/synth/main.go
// proof size. It is the loom counterpart of plonky3's prove_prime_field_31
// example: both stacks build a deg-2 row-local constraint over a tall or
// wide trace and use matching FRI parameters, so the two can be compared
// apple-to-apple.
Comment thread bench/synth/main.go
Comment on lines +137 to +143
fmt.Println("prove-phase breakdown:")
for _, p := range phases {
share := 100 * float64(p.d) / float64(proveWall)
fmt.Printf(" %-26s %s %5.1f%%\n", p.name, fmtDur(p.d), share)
}
sort.Slice(phases, func(i, j int) bool { return phases[i].d > phases[j].d })

Comment on lines +30 to +32
// quotientParallelThreshold is the smallest size (in koalabear ops) for
// which fan-out across goroutines is worth the scheduler overhead.
const quotientParallelThreshold = 1 << 12
Comment thread internal/poly/utils.go
Comment on lines +29 to +33
// PowUint64 returns base^exp in koalabear via binary exponentiation.
// Used to seed per-chunk accumulators when parallelizing loops that
// otherwise carry a serial multiplicative dependency (a_{i+1} = a_i · g).
func PowUint64(base koalabear.Element, exp uint64) koalabear.Element {
var res koalabear.Element

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 41d6eb3. Configure here.

Comment thread bench/synth/main.go
share := 100 * float64(p.d) / float64(proveWall)
fmt.Printf(" %-26s %s %5.1f%%\n", p.name, fmtDur(p.d), share)
}
sort.Slice(phases, func(i, j int) bool { return phases[i].d > phases[j].d })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead sort after printing makes output unsorted

Low Severity

The sort.Slice on phases is performed after the phases have already been printed (lines 138–141), so it has no visible effect. If the intent was to display phases sorted by duration, the sort needs to precede the print loop. As written, it's dead code whose result is never consumed.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 41d6eb3. Configure here.

@ThomasPiellard ThomasPiellard merged commit 41d6eb3 into main May 28, 2026
2 checks passed
@ThomasPiellard ThomasPiellard deleted the perf/parallel-lde-merkle-fri branch May 28, 2026 12:27
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.

3 participants