perf: row-chunk per-node DAG vector evaluation#12
Open
gbotrel wants to merge 1 commit into
Open
Conversation
ComputeAIRQuotients (which is 41% of tall wall and 68% of wide wall on
100M-cell synth traces) drives a per-row sweep through every DAG node
in the vanishing relation. The outer per-node walk is necessarily
serial (later nodes consume earlier ones), but every per-node row
sweep is embarrassingly parallel — disjoint slots in dst, read-only
access to the child vectors.
Wrap every per-row loop in evalBaseNodeOnAllEntries,
evalExtNodeOnAllEntries, the lift/add/sub/mul child-vector helpers,
fillBaseLeafVector and fillExtLeafVector with
parallel.ExecuteWithThreshold(N, dagRowParallelThreshold=1<<12, …).
For ext.Vector.{Add,Sub,Mul,MulByElement} (already SIMD-vectorised)
the chunks just re-enter the same SIMD primitive on a slice.
The DAG eval workspace's basePool/extPool allocators run only in the
outer (serial) loop, so no concurrency protection is needed there.
Bench on a 32-core EPYC 9R45, synth 100M cells, median of multiple
runs:
shape before after speedup
tall (2^22 × 24) 5.55 s 4.44 s 1.25×
wide (2^16 × 1536) 1.25 s 1.07 s 1.17×
Phase breakdown — compute-air-quotients:
tall 2.29 s → 1.35 s 1.70×
wide 0.85 s → 0.66 s 1.29×
Tests: go test ./... + -race ./internal/dag ./internal/poly ./prover —
all clean. Existing prover end-to-end tests verify the parallel and
serial paths agree (Prove → Verify).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR focuses on improving prover throughput by adding row-chunked parallelism to the hottest per-row loops (notably DAG evaluation and DEEP quotient assembly), while also capping nested FFT parallelism to avoid goroutine explosion. It also adds lightweight phase timing instrumentation and new synthetic benchmarks/docs to measure and track performance wins.
Changes:
- Parallelize per-row sweeps in DAG evaluation and other per-entry loops using chunked
parallel.ExecuteWithThreshold. - Cap inner FFT parallelism (
fft.WithNbTasks) when FFTs run inside higher-level parallel fan-outs. - Add
Config.PhaseCallbackfor per-phase wall timing, plus new synthetic benchmarks and an optimization plan doc.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
prover/prover.go |
Adds phase timing callback; caps per-chunk FFT tasks; parallelizes evaluations-at-zeta; refactors/parallelizes DEEP quotient accumulation. |
prover/bench_deep_quotient_test.go |
Adds wide/tall microbenchmarks for Prove focused on DEEP quotient-dominant regimes. |
plan_optims.md |
Adds a detailed performance optimization plan and measurement procedure. |
internal/dag/dag.go |
Introduces row-chunked parallelism in base/ext DAG node evaluation and leaf filling helpers. |
internal/poly/compute_quotient.go |
Parallelizes FFT-heavy rails and coset conversions; adds thresholds and FFT task capping. |
internal/poly/ext.go |
Adds optional FFT options to evaluation helpers to cap inner FFT parallelism. |
internal/reedsolomon/rs.go |
Adds optional FFT options passthrough to encoder FFTs for nested parallelism control. |
internal/poly/utils.go |
Adds PowUint64 helper for fast exponentiation used to break serial dependencies. |
internal/poly/utils_test.go |
Adds tests validating PowUint64 correctness. |
internal/parallel/parallel.go |
Adds NbTasksPerJob and ExecuteWithThreshold helpers for better nested-parallel control. |
internal/merkle/tree.go |
Parallelizes bottom-up internal-node hashing once levels are wide enough. |
internal/fri/fri.go |
Uses shared leaf hashing helper and parallelizes fold loops with chunk seeding. |
internal/commitment/commitment.go |
Parallelizes RS encoding; adds HashLeavesParallel helper and caps FFT inner tasks. |
bench/synth/main.go |
Adds a standalone synthetic prover/verify benchmark driver with optional profiling output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+664
to
+668
| 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) |
Comment on lines
766
to
+770
| for j, shift := range dqLayout.Shifts[i] { | ||
| var omegaShift koalabear.Element | ||
| omegaShift.SetOne() | ||
| for k := 0; k < shift; k++ { | ||
| omegaShift.Mul(&omegaShift, &domainN.Generator) | ||
| } | ||
| z_s := pr.zeta | ||
| z_s.MulByElement(&z_s, &omegaShift) | ||
|
|
||
| C_s := make(poly.ExtPolynomial, N) | ||
| var v_s ext.E4 | ||
| names := dqLayout.Names[i][j] | ||
| keys := dqLayout.Keys[i][j] | ||
| for k := range names { | ||
| evalAtZ, ok := pr.Proof.ValueAtZetaExt(keys[k]) | ||
| if !ok { | ||
| return fmt.Errorf("ComputeDeepQuotient: %q not found in ValuesAtZeta", keys[k]) | ||
| } | ||
| colExt, hasExt := pr.t.Ext[names[k]] | ||
| colBase, hasBase := pr.t.Base[names[k]] | ||
| if !hasExt && !hasBase { | ||
| return fmt.Errorf("ComputeDeepQuotient: column %q not found in trace", names[k]) | ||
| } | ||
| if hasExt { | ||
| addScaledExtColumn(C_s, colExt, scratch, &alphaAcc) | ||
| } else { | ||
| addScaledBaseColumn(C_s, colBase, &alphaAcc) | ||
| } | ||
| var term ext.E4 | ||
| term.Mul(&evalAtZ, &alphaAcc) | ||
| v_s.Add(&v_s, &term) | ||
| alphaAcc.Mul(&alphaAcc, &pr.alpha) | ||
| } | ||
|
|
||
| DQ_s := poly.DeepQuotientExt(C_s, v_s, z_s, domainN) | ||
| for x := 0; x < N; x++ { | ||
| deepQuotient[x].Add(&deepQuotient[x], &DQ_s[x]) | ||
| omegaShift.Exp(domainN.Generator, big.NewInt(int64(shift))) | ||
| zs := pr.zeta | ||
| zs.MulByElement(&zs, &omegaShift) |
Comment on lines
+908
to
+912
| if start == 0 { | ||
| omegaX.SetOne() | ||
| } else { | ||
| omegaX.Exp(domain.Generator, big.NewInt(int64(start))) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step 4 of `plan_optims.md`. `ComputeAIRQuotients` was 41% of tall wall and 68% of wide wall on 100M-cell synth traces. The phase drives a per-row sweep through every DAG node — the outer per-node walk is necessarily serial (later nodes consume earlier ones) but every per-node row sweep is embarrassingly parallel: disjoint slots in `dst`, read-only access to the child vectors.
Wrap every per-row loop in `evalBaseNodeOnAllEntries`, `evalExtNodeOnAllEntries`, the lift/add/sub/mul child-vector helpers, and the leaf fillers with `parallel.ExecuteWithThreshold(N, dagRowParallelThreshold = 1 << 12, …)`. For `ext.Vector.{Add,Sub,Mul,MulByElement}` (already SIMD-vectorised) the chunks just re-enter the same SIMD primitive on a slice.
The DAG eval workspace's `basePool` / `extPool` allocators run only in the outer (serial) loop, so no concurrency protection needed.
Numbers
32-core EPYC 9R45, synth bench at 100 M cells, median of multiple runs:
`compute-air-quotients` phase:
Test plan
🤖 Generated with Claude Code
Note
Low Risk
Changes are CPU parallelism and benchmarking only; proof correctness relies on existing end-to-end tests rather than touching security-sensitive logic.
Overview
This PR is a prover performance pass: it parallelizes hot loops across the proof pipeline and adds tooling to measure wins.
AIR / quotient path:
EvalOnAllEntriesMixedrow work in the DAG is chunked withparallel.ExecuteWithThreshold(threshold1 << 12) for base and extension node eval, leaf fills, and child vector ops—node order stays serial; rows fan out in parallel. Quotient computation parallelizes FFT/coset steps and divides byX^N-1, withfft.WithNbTaskscapped viaNbTasksPerJobso nested parallelism does not oversubscribe CPUs.Commitments, Merkle, FRI: RS encode and leaf hashing run in parallel;
HashLeavesParallelcentralizes batched/scalar leaf hashing. Merkle internal nodes hash per level in parallel above a width threshold. FRI fold layers parallelize with per-chunkGeneratorInvseeding through newpoly.PowUint64.Prover: Optional
WithPhaseCallbackreports phase wall times. AIR quotients, evaluations at ζ, and DEEP quotient accumulation gain parallel task lists; DEEP work is refactored into bundles plus row-parallelaccumulateDeepQuotientwith batched inverses.bench/synthandBenchmarkProveWide/BenchmarkProveTallsupport apples-to-apples benchmarking;plan_optims.mddocuments follow-on optimizations.Risk: Low for product/security surface (no auth or wire-format changes in the DAG path); correctness still depends on identical field math under concurrency—existing prove/verify tests and race runs are the main guardrails.
Reviewed by Cursor Bugbot for commit dcdada8. Bugbot is set up for automated code reviews on this repo. Configure here.