perf: fan out ComputeEvaluationsAtZeta across columns#8
Conversation
Each per-column evaluation at ζ is an independent FFTInverse + Horner pass.
The previous parallel.Execute only fanned out over modules — single-module
programs (the common shape, including the synth wide benchmark) saw every
column evaluated serially in one goroutine.
Gather every (poly, evaluation point) pair across all modules into a single
task list and parallel.Execute over it. Inner FFTs run with WithNbTasks(1)
since the column-level fan-out already saturates the CPUs; the FFT option
is forwarded through new variadic fft.Option args on poly.EvaluateAtExt
and poly.ExtEvaluateAtExt.
Drive-by: replace the O(shift) manual mul loop for the rotation twiddle
with Element.Exp (binary exponentiation).
Wide-trace benchmark (synth, log2_rows=12, repetitions=256, 32 cores):
BenchmarkProveWide 232.8 ms → 178.0 ms (1.31×)
prover/bench_evals_test.go is added so the wide-shape improvement remains
visible in `go test -bench`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves prover throughput by parallelizing ComputeEvaluationsAtZeta across individual column evaluations (instead of only across modules), adds a way to cap inner FFT parallelism via forwarded fft.Options, and introduces a wide-trace benchmark to keep the speedup visible in go test -bench.
Changes:
- Refactor
ComputeEvaluationsAtZetato build a flat per-column task list andparallel.Executeover it, while capping inner FFTs tofft.WithNbTasks(1). - Extend
poly.EvaluateAtExt/poly.ExtEvaluateAtExtwith variadicfft.Optionforwarding for inner-FFT tuning. - Add
BenchmarkProveWideto exercise the “single-module, many columns” shape where evaluations-at-zeta dominates.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
prover/prover.go |
Flattens and parallelizes evaluation-at-ζ work across columns; caps inner FFT parallelism. |
internal/poly/ext.go |
Adds variadic FFT options to evaluation helpers so callers can control FFT tasking. |
prover/bench_evals_test.go |
Adds a wide-trace benchmark to measure/track the evals-at-ζ speedup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| taskCap := 0 | ||
| for i, moduleName := range moduleNames { | ||
| leaves := pr.program.Modules[moduleName].VanishingRelation.LeavesFull(config) | ||
| moduleLeaves[i] = leaves | ||
| taskCap += len(leaves) | ||
| } |
| if leaf.Type == expr.RotatedColumn { | ||
| shift := ((leaf.Shift % module.N) + module.N) % module.N | ||
| var omegaPow koalabear.Element | ||
| omegaPow.Exp(module.D.Generator, big.NewInt(int64(shift))) | ||
| evalPoint.MulByElement(&evalPoint, &omegaPow) |
|
looks good, but I want to test something else first -> compute all the Lagrange at zeta for the biggest module size once, and then use scalar products of a subset of those Lagrange at zeta (for smaller modules) to do batch evaluation directly |
Summary
Each per-column evaluation at ζ is an independent FFTInverse + Horner pass. The previous
parallel.Executeonly fanned out over modules — single-module programs (the common shape, including the synth wide benchmark) saw every column evaluated serially in one goroutine. After step 1 (#7), evaluations-at-zeta is what the bench report flagged as the next-biggest serial chunk (~57% of wide-trace prove wall).(poly, eval point)task across all modules into one flat list;parallel.Executeover it.fft.WithNbTasks(1)— the column-level fan-out already saturates the cores. The FFT option is forwarded through new variadicfft.Optionargs onpoly.EvaluateAtExt/poly.ExtEvaluateAtExt.Element.Exp(binary exponentiation).prover/BenchmarkProveWideso the improvement stays visible ingo test -bench.Numbers
Wide-trace benchmark (synth,
log2_rows=12,repetitions=256, 32 cores):Test plan
go test ./... -count=1— all greengo vet ./...— only pre-existingcopylockswarningsgo test -bench=BenchmarkProveWide -benchtime=5x ./prover/— 1.31× speedupBenchmarkProverinintegration_test/gnark_plonk— unchanged (the existing multi-module path was already parallel)🤖 Generated with Claude Code
Note
Medium Risk
Changes hot-path prove logic and parallel FFT behavior; correctness still depends on identical evaluation math, but oversubscription and ordering bugs would surface as proof failures rather than compile errors.
Overview
ComputeEvaluationsAtZetano longer parallelizes only by module. It builds one flat list of per-column (and AIR chunk) evaluation tasks across all modules, runsparallel.Executeover that list, and writes results back into the proof. Each task still does FFTInverse + Horner viapoly.EvaluateAtExt/poly.ExtEvaluateAtExt, but inner FFTs are forced to a single worker withfft.WithNbTasks(1)so nested parallelism does not oversubscribe CPUs.Those poly helpers now accept optional
fft.Optionvariadic args and forward them intoFFTInverse/FFTInverseExt. Rotated-column evaluation points useomegaPow.Exp(module.D.Generator, …)instead of repeated multiplies.A new
BenchmarkProveWideinprovertargets wide single-module traces where evaluations-at-ζ dominate prove time, so the column fan-out stays measurable ingo test -bench.Reviewed by Cursor Bugbot for commit 3dfefbf. Bugbot is set up for automated code reviews on this repo. Configure here.