Skip to content

perf: share precomputed zeta-powers across ComputeEvaluationsAtZeta#13

Open
gbotrel wants to merge 2 commits into
perf/all-combinedfrom
perf/zeta-powers
Open

perf: share precomputed zeta-powers across ComputeEvaluationsAtZeta#13
gbotrel wants to merge 2 commits into
perf/all-combinedfrom
perf/zeta-powers

Conversation

@gbotrel

@gbotrel gbotrel commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Step 5 of `plan_optims.md`. The Horner loop in `EvaluateAtExt` / `ExtEvaluateAtExt` is sequential per task — n ext-mul + n ext-add + a `bits.Reverse64` per row. pprof showed it at 10–16% of total CPU on the synth bench, second only to the Merkle-node permutation.

Replace with one SIMD-accelerated inner product:

  • Precompute the bit-reversed powers of zeta once, shared across every task that evaluates an n-row polynomial at the same point. Build cost is amortised over all tasks (typically 1–2 distinct (n, zeta) pairs per ComputeEvaluationsAtZeta call); construction itself is row-chunked via `powUint64Ext` binary-exponentiation seeding.
  • Per-task work becomes `d.FFTInverse(_p, fft.DIF, ...)` followed by `ext.Vector(zPow).InnerProductByElement(_p)` (or `InnerProduct` on the ext rail). Both have AVX-512 paths in gnark-crypto.

Powers are stored in bit-reversed-canonical order so the DIF FFT output can feed straight into the inner product — no extra `BitReverse` pass.

Numbers

32-core EPYC 9R45, synth bench at 100 M cells, median of 3:

shape before after speedup
tall (2²² × 24) 5.55 s 4.82 s 1.15×
wide (2¹⁶ × 1536) 1.25 s 1.20 s 1.04×

`evaluations-at-zeta` phase:

shape before after speedup
tall 450 ms 149 ms 3.0×
wide 57 ms 7 ms 8.1×

The phase-level speedup overshoots prior estimates because Horner was paying for both ext-mul-chain and per-row `bits.Reverse64` indexing; the SIMD inner product retires both.

Test plan

  • `go test ./... -count=1` — all green
  • `go test -race ./internal/poly ./prover` — no races
  • Every existing prover end-to-end test exercises `Verify` — an off-by-one in the power-table order or a wrong-rail inner product would surface

🤖 Generated with Claude Code


Note

Medium Risk
Changes core polynomial evaluation math on the proving hot path; wrong power ordering would break proofs, but new tests parity-check against the prior Horner implementation.

Overview
Speeds up evaluations-at-zeta by replacing the per-column Horner loop after FFTInverse with a shared, bit-reversed ζ-power table and a single SIMD inner product (InnerProductByElement / InnerProduct).

poly gains BuildZPowBitReversed (parallel O(n) build, bit-reversed order to match DIF output) and EvaluateAtExtWithZPow / ExtEvaluateAtExtWithZPow. ComputeEvaluationsAtZeta caches one table per distinct (n, evalPoint) and uses the fast path for n > 1, keeping the old evaluators only for length-1 polynomials.

Tests assert the new path matches EvaluateAtExt / ExtEvaluateAtExt across sizes and ζ values.

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

…asks

The Horner loop in EvaluateAtExt / ExtEvaluateAtExt runs sequentially
inside each task: n ext-multiplies + n ext-adds + a bits.Reverse64 per
row. Profiling showed this loop at 10–16% flat CPU on the synth bench
(8.5s of CPU on tall) — second only to the Merkle-node permutation.

Replace it with one SIMD-accelerated InnerProduct: precompute the
bit-reversed powers of zeta once (shared across every task that
evaluates an n-row polynomial at the same point), then the per-task
inner work becomes:

  d.FFTInverse(_p, fft.DIF, ...)          // unchanged
  res = ext.Vector(zPowBitRev).InnerProductByElement(_p)

Both Vector.InnerProduct (ext × ext) and Vector.InnerProductByElement
(ext × base) have AVX-512 paths in gnark-crypto.

Powers are computed in parallel via chunked binary-exponentiation
seeding (powUint64Ext) and permuted into bit-reversed order to match
the FFTInverse DIF output, so EvaluateAtExtWithZPow needs no extra
BitReverse pass.

Numbers, 32-core EPYC 9R45, synth (100M cells), median of 3:

  shape                       before    after     speedup
  tall (2^22 × 24)            5.55 s    4.82 s    1.15×
  wide (2^16 × 1536)          1.25 s    1.20 s    1.04×

Phase breakdown — evaluations-at-zeta:
  tall   450 ms → 149 ms    3.0×
  wide    57 ms →   7 ms    8.1×

Tests: go test ./... + -race ./internal/poly ./prover. Existing prover
end-to-end tests exercise Verify on every Prove; an off-by-one in the
power-table order or a wrong-rail inner-product would surface.

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

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 aims to improve prover performance by adding shared zeta-power evaluation and broadening parallelism across several prover hot paths. It also adds benchmarking/profiling infrastructure to measure phase-level performance.

Changes:

  • Adds shared bit-reversed zeta-power tables and SIMD inner-product evaluation for evaluations at zeta.
  • Introduces parallelism/capped FFT task plumbing in Merkle, FRI, RS commitment, quotient, and DEEP quotient paths.
  • Adds phase callbacks, synthetic benchmark tooling, and optimisation planning documentation.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
prover/prover.go Reworks zeta evaluations, AIR quotient FFT task caps, DEEP quotient accumulation, and phase timing callbacks.
prover/bench_deep_quotient_test.go Adds synthetic wide/tall prover benchmarks.
plan_optims.md Adds optimisation roadmap and measurement guidance.
internal/reedsolomon/rs.go Threads optional FFT options through RS encoding.
internal/poly/utils.go Adds base-field binary exponentiation helper.
internal/poly/utils_test.go Adds tests for PowUint64.
internal/poly/ext.go Adds zeta-power table construction and WithZPow evaluation paths.
internal/poly/compute_quotient.go Adds quotient parallel thresholds and parallel FFT/scaling loops.
internal/parallel/parallel.go Adds task-budget and thresholded execution helpers.
internal/merkle/tree.go Parallelizes internal-node tree construction by level.
internal/fri/fri.go Parallelizes FRI folding and reuses parallel leaf hashing.
internal/commitment/commitment.go Parallelizes RS encoding and exposes parallel leaf hashing helper.
bench/synth/main.go Adds a synthetic proving benchmark/profiling command.

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

Comment thread internal/poly/ext.go
// zeta have been precomputed by the caller (typically once per (n, zeta)
// across many polynomials). Replaces the per-Horner ext-mul chain with a
// single SIMD InnerProductByElement on the FFTInverse output.
func EvaluateAtExtWithZPow(p Polynomial, d *fft.Domain, zPowBitRev []ext.E4, fftOpts ...fft.Option) ext.E4 {
Comment thread plan_optims.md

## House rules for the agent picking this up

- **One step = one PR**. Don't bundle. Each PR's review surface stays under ~300 LOC if possible.
@gbotrel gbotrel changed the base branch from main to perf/all-combined May 27, 2026 22:15
PR #13 review feedback (Copilot): the new bit-reversed power-table
evaluators only had end-to-end coverage through the prover. Add
internal/poly tests that build zPow and cross-check
{Ext,}EvaluateAtExtWithZPow against the existing Horner evaluators
across multiple sizes and zeta values (incl. identity / mixed coords).
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.

2 participants