Skip to content

perf: row-chunk per-node DAG vector evaluation#12

Open
gbotrel wants to merge 1 commit into
perf/all-combinedfrom
perf/parallel-dag-eval
Open

perf: row-chunk per-node DAG vector evaluation#12
gbotrel wants to merge 1 commit into
perf/all-combinedfrom
perf/parallel-dag-eval

Conversation

@gbotrel

@gbotrel gbotrel commented May 27, 2026

Copy link
Copy Markdown
Contributor

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:

shape before after speedup
tall (2²² × 24) 5.55 s 4.44 s 1.25×
wide (2¹⁶ × 1536) 1.25 s 1.07 s 1.17×

`compute-air-quotients` phase:

shape before after speedup
tall 2.29 s 1.35 s 1.70×
wide 0.85 s 0.66 s 1.29×

Test plan

  • `go test ./... -count=1` — all green
  • `go test -race ./internal/dag ./internal/poly ./prover` — no races
  • Every existing Prover end-to-end test exercises `Verify` after `Prove` — algebraic mismatch in the parallel sweep would surface there

🤖 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: EvalOnAllEntriesMixed row work in the DAG is chunked with parallel.ExecuteWithThreshold (threshold 1 << 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 by X^N-1, with fft.WithNbTasks capped via NbTasksPerJob so nested parallelism does not oversubscribe CPUs.

Commitments, Merkle, FRI: RS encode and leaf hashing run in parallel; HashLeavesParallel centralizes batched/scalar leaf hashing. Merkle internal nodes hash per level in parallel above a width threshold. FRI fold layers parallelize with per-chunk GeneratorInv seeding through new poly.PowUint64.

Prover: Optional WithPhaseCallback reports phase wall times. AIR quotients, evaluations at ζ, and DEEP quotient accumulation gain parallel task lists; DEEP work is refactored into bundles plus row-parallel accumulateDeepQuotient with batched inverses. bench/synth and BenchmarkProveWide / BenchmarkProveTall support apples-to-apples benchmarking; plan_optims.md documents 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.

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>
Copilot AI review requested due to automatic review settings May 27, 2026 21:31

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 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.PhaseCallback for 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 thread prover/prover.go
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 thread prover/prover.go
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 thread prover/prover.go
Comment on lines +908 to +912
if start == 0 {
omegaX.SetOne()
} else {
omegaX.Exp(domain.Generator, big.NewInt(int64(start)))
}
@gbotrel gbotrel changed the base branch from main to perf/all-combined May 27, 2026 22:15
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