perf: parallelize LDE / coset-FFT / Merkle pipeline#7
Conversation
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>
There was a problem hiding this comment.
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.PowUint64for fast exponentiation used to seed per-chunk accumulators in parallel loops. - Add
prover.WithPhaseCallbackand a newbench/synthcommand 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.
| // 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. |
| 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 }) | ||
|
|
| // quotientParallelThreshold is the smallest size (in koalabear ops) for | ||
| // which fan-out across goroutines is worth the scheduler overhead. | ||
| const quotientParallelThreshold = 1 << 12 |
| // 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 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
| 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 }) |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 41d6eb3. Configure here.


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(orExecuteWithThresholdfor 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:
Phase-level for tall:
Verifier wall is unchanged (~1.4 ms tall / 2.9 ms wide). Proof size unchanged.
Where each change lives
internal/commitment/— per-polynomialEncode/EncodeExtloops inRSCommit.Commitrun in parallel; exposedHashLeavesParallelso FRI can use the same batched (rate-16 Poseidon2) leaf-hashing path.internal/merkle/tree.go—Tree.BuildandBuildNodeshash each tree level in parallel above a width threshold (parallelLevelThreshold = 64); level-to-level walk stays serial (data dependency).internal/fri/fri.go—buildTree{Base,Ext}now use the batched parallel leaf hasher instead of a serial per-leafHashLeaf;foldLayer{Base,Ext}are chunked with per-chunkxInv = GeneratorInv^chunkStartseeded viapoly.PowUint64.internal/poly/compute_quotient.go—ComputeQuotientandComputeQuotientMixedfan out every per-polynomial FFT / FFTInverse pass and the final per-element divide;CosetLagrangeNormalToCanonical{,Ext}WithCacheparallelize theacc · invFrGen^kloop via chunkedinvFrGenseeding.internal/parallel/parallel.go— smallExecuteWithThreshold(n, threshold, work)helper to replace the repeated "if n ≥ threshold fan out, else serial" pattern.internal/poly/utils.go—PowUint64(base, exp)binary exponentiation used to seed parallel chunks.prover/prover.go— newWithPhaseCallback(fn)option; the prover firesfn(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 onproverRuntime(unrelated to this PR)go run ./bench/synth -log2-rows 20 -repetitions 8verifies and prints the new phase breakdowngo run ./bench/synth -log2-rows 14 -repetitions 512samego run ./bench) still runs end-to-end and the produced proof verifiesNotes / follow-ups
evaluations-at-zeta(22% of tall, 57% of wide):EvaluateAtExtper column insideComputeEvaluationsAtZeta.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.ExecuteandExecuteWithThreshold, with chunk seeding (poly.PowUint64) where loops had serial multiplicative state (xInv,invFrGen^k).Commitment & Merkle: per-polynomial Reed–Solomon
Encode/EncodeExtand leaf hashing fan out;HashLeavesParallelis shared by RS commit and FRI tree builds. MerkleBuildhashes wide internal levels in parallel above a width threshold.FRI & poly:
foldLayerBase/Extand quotient/coset paths parallelize FFT passes and final divides; coset-to-canonical scaling is parallelized the same way.Observability & bench:
prover.WithPhaseCallbackreports wall time per major prove phase; newbench/synthproves 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.