diff --git a/bench/synth/main.go b/bench/synth/main.go new file mode 100644 index 0000000..e4c916a --- /dev/null +++ b/bench/synth/main.go @@ -0,0 +1,177 @@ +// Command synth proves a synthetic AIR with configurable trace shape +// (rows × cols) and reports per-phase wall time, prove/verify totals and +// 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. +// +// Per row group of 3 columns (a, b, c), one constraint is enforced: +// +// a * b - c = 0 +// +// Run examples: +// +// go run ./bench/synth -log2-rows 20 -repetitions 8 # tall +// go run ./bench/synth -log2-rows 14 -repetitions 512 # wide +package main + +import ( + "bytes" + "encoding/gob" + "flag" + "fmt" + "os" + "runtime" + "sort" + "sync" + "time" + + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/loom" + "github.com/consensys/loom/board" + "github.com/consensys/loom/expr" + "github.com/consensys/loom/prover" + "github.com/consensys/loom/setup" + "github.com/consensys/loom/trace" + "github.com/consensys/loom/verifier" +) + +var ( + log2Rows = flag.Int("log2-rows", 20, "log2 of trace height (rows)") + repetitions = flag.Int("repetitions", 16, "number of (a,b,c) tuples per row; trace width = 3 * repetitions") + hashName = flag.String("hash", "poseidon2", "hash backend: poseidon2 | sha256") + gomaxprocs = flag.Int("gomaxprocs", 0, "override GOMAXPROCS (0 = leave default)") +) + +const moduleName = "synth" + +func colName(i int) string { return fmt.Sprintf("%s.c_%d", moduleName, i) } + +func main() { + flag.Parse() + + if *gomaxprocs > 0 { + runtime.GOMAXPROCS(*gomaxprocs) + } + procs := runtime.GOMAXPROCS(0) + rows := 1 << *log2Rows + width := 3 * *repetitions + + fmt.Printf("loom synth rows=2^%d (=%d) width=%d (3 cols × %d reps) cells=%d hash=%s GOMAXPROCS=%d NumCPU=%d\n\n", + *log2Rows, rows, width, *repetitions, rows*width, *hashName, procs, runtime.NumCPU()) + + // Build module + compile program. + builder := board.NewBuilder() + m := board.NewModule(moduleName) + m.N = rows + for k := 0; k < *repetitions; k++ { + a := expr.Col(colName(3 * k)) + b := expr.Col(colName(3*k + 1)) + c := expr.Col(colName(3*k + 2)) + m.AssertZero(a.Mul(b).Sub(c)) // a*b - c = 0 + } + builder.AddModule(m) + program, err := board.Compile(&builder) + if err != nil { + fail("Compile: %v", err) + } + + // Synthesize trace (column-major). Values are derived from cheap + // deterministic arithmetic so trace generation isn't the bottleneck. + t := trace.New(width) + for k := 0; k < *repetitions; k++ { + a := make([]koalabear.Element, rows) + b := make([]koalabear.Element, rows) + c := make([]koalabear.Element, rows) + for i := 0; i < rows; i++ { + a[i].SetUint64(uint64(i + 1 + k)) + b[i].SetUint64(uint64(2*i + 3 + k)) + c[i].Mul(&a[i], &b[i]) + } + t.SetBase(colName(3*k), a) + t.SetBase(colName(3*k+1), b) + t.SetBase(colName(3*k+2), c) + } + + hashBackend := resolveHashBackend(*hashName) + + // Prove with phase timings collected via the public WithPhaseCallback option. + type phase struct { + name string + d time.Duration + } + var ( + mu sync.Mutex + phases []phase + ) + opts := []prover.Option{ + prover.WithHashBackend(hashBackend), + prover.WithPhaseCallback(func(name string, d time.Duration) { + mu.Lock() + phases = append(phases, phase{name, d}) + mu.Unlock() + }), + } + + t0 := time.Now() + prf, err := prover.Prove(t, setup.ProvingKey{}, nil, program, opts...) + if err != nil { + fail("Prove: %v", err) + } + proveWall := time.Since(t0) + + // Serialize for size, then verify. + var buf bytes.Buffer + if err := gob.NewEncoder(&buf).Encode(&prf); err != nil { + fail("encode proof: %v", err) + } + + t0 = time.Now() + if err := verifier.Verify(nil, setup.VerificationKey{}, program, prf, + verifier.WithHashBackend(hashBackend)); err != nil { + fail("Verify: %v", err) + } + verifyWall := time.Since(t0) + + // Report. + 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 }) + + fmt.Printf("\nprove wall : %s\n", fmtDur(proveWall)) + fmt.Printf("verify wall: %s\n", fmtDur(verifyWall)) + fmt.Printf("proof size : %d B (gob)\n", buf.Len()) + fmt.Printf("proof : %d commitments, %d FRI levels, %d query samplings\n", + len(prf.Commitments), len(prf.DeepQuotientCommitment), len(prf.PointSamplings)) +} + +func resolveHashBackend(name string) loom.HashBackend { + switch name { + case "poseidon2": + return loom.Poseidon2HashBackend() + case "sha256": + return loom.SHA256HashBackend() + default: + fail("unknown hash backend %q (want poseidon2 | sha256)", name) + return loom.HashBackend{} + } +} + +func fmtDur(d time.Duration) string { + switch { + case d >= time.Second: + return fmt.Sprintf("%.2fs", d.Seconds()) + case d >= time.Millisecond: + return fmt.Sprintf("%.1fms", float64(d.Microseconds())/1000) + default: + return d.String() + } +} + +func fail(format string, args ...any) { + fmt.Fprintf(os.Stderr, "synth: "+format+"\n", args...) + os.Exit(1) +} diff --git a/internal/commitment/commitment.go b/internal/commitment/commitment.go index 7820621..744ecad 100644 --- a/internal/commitment/commitment.go +++ b/internal/commitment/commitment.go @@ -202,18 +202,23 @@ func (rs *RSCommit) Commit(basePolys []poly.Polynomial, extPolys []poly.ExtPolyn domainCache = &poly.DomainCache{} } - // 1- encode every polynomial on its rail + // 1- encode every polynomial on its rail. Each Encode is independent + // (disjoint input/output slices, shared read-only domain). encodedBase := make([]poly.Polynomial, len(basePolys)) - for i, pol := range basePolys { - n := len(pol) - encodedBase[i] = rs.Encoder.Encode(pol, domainCache.Get(uint64(n))) - } + parallel.Execute(len(basePolys), func(start, end int) { + for i := start; i < end; i++ { + pol := basePolys[i] + encodedBase[i] = rs.Encoder.Encode(pol, domainCache.Get(uint64(len(pol)))) + } + }) encodedExt := make([]poly.ExtPolynomial, len(extPolys)) - for i, pol := range extPolys { - n := len(pol) - encodedExt[i] = rs.Encoder.EncodeExt(pol, domainCache.Get(uint64(n))) - } + parallel.Execute(len(extPolys), func(start, end int) { + for i := start; i < end; i++ { + pol := extPolys[i] + encodedExt[i] = rs.Encoder.EncodeExt(pol, domainCache.Get(uint64(len(pol)))) + } + }) // 2- build the merkle tree, with rs.N/2 leafs // the i-th leaf is base pairs followed by extension pairs. @@ -235,13 +240,7 @@ func (rs *RSCommit) Commit(basePolys []poly.Polynomial, extPolys []poly.ExtPolyn Ext: encodedExt, PairOffset: halfN, } - if batchHasher, ok := rs.LeafHasher.(BatchLeafHasher); ok { - hashLeavesBatchParallel(batchHasher, leaves, src) - } else { - parallel.Execute(halfN, func(start, end int) { - hashLeavesScalar(rs.LeafHasher, leaves[start:end], src, start) - }) - } + HashLeavesParallel(rs.LeafHasher, leaves, src) if err := tree.Build(leaves); err != nil { return WMerkleTree{}, err @@ -250,6 +249,19 @@ func (rs *RSCommit) Commit(basePolys []poly.Polynomial, extPolys []poly.ExtPolyn return wTree, nil } +// HashLeavesParallel hashes len(dst) paired leaves from src into dst, using +// the batched leaf hasher when available (rate-16 Poseidon2 sponge) and +// fanning the work out across goroutines. +func HashLeavesParallel(lh LeafHasher, dst []hash.Digest, src LeafSource) { + if batchHasher, ok := lh.(BatchLeafHasher); ok { + hashLeavesBatchParallel(batchHasher, dst, src) + return + } + parallel.Execute(len(dst), func(start, end int) { + hashLeavesScalar(lh, dst[start:end], src, start) + }) +} + func hashLeavesBatchParallel(lh BatchLeafHasher, dst []hash.Digest, src LeafSource) { batchSize := lh.BatchSize() if batchSize <= 0 { diff --git a/internal/fri/fri.go b/internal/fri/fri.go index aad8181..5bf0f26 100644 --- a/internal/fri/fri.go +++ b/internal/fri/fri.go @@ -26,9 +26,15 @@ import ( fiatshamir "github.com/consensys/loom/internal/fiat-shamir" "github.com/consensys/loom/internal/hash" "github.com/consensys/loom/internal/merkle" + "github.com/consensys/loom/internal/parallel" + "github.com/consensys/loom/internal/poly" "github.com/consensys/loom/internal/reedsolomon" ) +// foldParallelThreshold is the smallest half-layer size at which fan-out +// across goroutines beats the precomputed-xInv seeding overhead per chunk. +const foldParallelThreshold = 1 << 12 + // Params holds the FRI configuration and precomputed per-level data. // Build once with NewParams; reuse across many Prove/Verify calls. type Params struct { @@ -855,7 +861,7 @@ func log2(n int) int { } // buildTreeBase builds a Merkle tree of Nⱼ/2 leaves where -// leaf k = LeafHasher(encode(layer[k]) || encode(layer[k + Nⱼ/2])). +// leaf k = LeafHasher(layer[k] || layer[k + Nⱼ/2]). func buildTreeBase(layer []koalabear.Element, lh commitment.LeafHasher, nh commitment.NodeHasher) (*merkle.Tree, error) { half := len(layer) / 2 tree, err := merkle.New(half, nh) @@ -863,10 +869,10 @@ func buildTreeBase(layer []koalabear.Element, lh commitment.LeafHasher, nh commi return nil, err } leaves := make([]hash.Digest, half) - for k := 0; k < half; k++ { - pair := []commitment.PairBase{{layer[k], layer[k+half]}} - leaves[k] = lh.HashLeaf(pair, nil) - } + commitment.HashLeavesParallel(lh, leaves, commitment.LeafSource{ + Base: []poly.Polynomial{layer}, + PairOffset: half, + }) return tree, tree.Build(leaves) } @@ -878,67 +884,65 @@ func buildTreeExt(layer []ext.E4, lh commitment.LeafHasher, nh commitment.NodeHa return nil, err } leaves := make([]hash.Digest, half) - for k := 0; k < half; k++ { - pair := []commitment.PairExt{{layer[k], layer[k+half]}} - leaves[k] = lh.HashLeaf(nil, pair) - } + commitment.HashLeavesParallel(lh, leaves, commitment.LeafSource{ + Ext: []poly.ExtPolynomial{layer}, + PairOffset: half, + }) return tree, tree.Build(leaves) } // foldLayerBase folds a base-field layer of size Nⱼ into a layer of size Nⱼ/2. +// +// The naive loop carries a serial dependency on xInv = GeneratorInv^i; each +// parallel chunk seeds xInv with GeneratorInv^chunkStart so chunks run +// independently. func foldLayerBase(layer []koalabear.Element, alpha koalabear.Element, domain *fft.Domain, invTwo koalabear.Element) []koalabear.Element { half := len(layer) / 2 next := make([]koalabear.Element, half) + parallel.ExecuteWithThreshold(half, foldParallelThreshold, func(start, end int) { + xInv := poly.PowUint64(domain.GeneratorInv, uint64(start)) + var sum, diff koalabear.Element + for i := start; i < end; i++ { + p, q := layer[i], layer[i+half] - var xInv, sum, diff koalabear.Element - xInv.SetOne() - - for i := 0; i < half; i++ { - p, q := layer[i], layer[i+half] - - sum.Add(&p, &q) - sum.Mul(&sum, &invTwo) - - diff.Sub(&p, &q) - diff.Mul(&diff, &invTwo) - diff.Mul(&diff, &xInv) - diff.Mul(&diff, &alpha) - - next[i].Add(&sum, &diff) + sum.Add(&p, &q) + sum.Mul(&sum, &invTwo) - xInv.Mul(&xInv, &domain.GeneratorInv) - } + diff.Sub(&p, &q) + diff.Mul(&diff, &invTwo) + diff.Mul(&diff, &xInv) + diff.Mul(&diff, &alpha) + next[i].Add(&sum, &diff) + xInv.Mul(&xInv, &domain.GeneratorInv) + } + }) return next } -// foldLayerExt folds an extension-field layer of size Nⱼ into a layer of size -// Nⱼ/2. Domain factors remain base-field elements and are multiplied with -// MulByElement. +// foldLayerExt is the extension-field counterpart of foldLayerBase; domain +// factors stay in the base field and are multiplied via MulByElement. func foldLayerExt(layer []ext.E4, alpha ext.E4, domain *fft.Domain, invTwo koalabear.Element) []ext.E4 { half := len(layer) / 2 next := make([]ext.E4, half) + parallel.ExecuteWithThreshold(half, foldParallelThreshold, func(start, end int) { + xInv := poly.PowUint64(domain.GeneratorInv, uint64(start)) + for i := start; i < end; i++ { + p, q := layer[i], layer[i+half] - var xInv koalabear.Element - xInv.SetOne() - - for i := 0; i < half; i++ { - p, q := layer[i], layer[i+half] + var sum, diff ext.E4 + sum.Add(&p, &q) + sum.MulByElement(&sum, &invTwo) - var sum, diff ext.E4 - sum.Add(&p, &q) - sum.MulByElement(&sum, &invTwo) - - diff.Sub(&p, &q) - diff.MulByElement(&diff, &invTwo) - diff.MulByElement(&diff, &xInv) - diff.Mul(&diff, &alpha) - - next[i].Add(&sum, &diff) - - xInv.Mul(&xInv, &domain.GeneratorInv) - } + diff.Sub(&p, &q) + diff.MulByElement(&diff, &invTwo) + diff.MulByElement(&diff, &xInv) + diff.Mul(&diff, &alpha) + next[i].Add(&sum, &diff) + xInv.Mul(&xInv, &domain.GeneratorInv) + } + }) return next } diff --git a/internal/merkle/tree.go b/internal/merkle/tree.go index a340d54..d5a7b5c 100644 --- a/internal/merkle/tree.go +++ b/internal/merkle/tree.go @@ -17,8 +17,13 @@ import ( "fmt" "github.com/consensys/loom/internal/hash" + "github.com/consensys/loom/internal/parallel" ) +// parallelLevelThreshold is the smallest level width (in nodes) at which +// fan-out beats serial bottom-up hashing. +const parallelLevelThreshold = 64 + type LeafHash = hash.Digest type NodeHash = hash.Digest @@ -72,10 +77,7 @@ func (t *Tree) BuildIthLeaf(leaf hash.Digest, i int) error { // BuildNodes call this function after all the BuildIthLeaf have been called func (t *Tree) BuildNodes() error { - n := t.nLeaves - for i := n - 1; i >= 1; i-- { - t.nodes[i] = t.nodeHasher.HashNode(t.nodes[2*i], t.nodes[2*i+1]) - } + t.buildInternalNodes() return nil } @@ -86,15 +88,26 @@ func (t *Tree) Build(leaves []hash.Digest) error { return fmt.Errorf("merkle: got %d leaves, want %d", len(leaves), t.nLeaves) } n := t.nLeaves - for i, leaf := range leaves { - t.nodes[n+i] = leaf - } - for i := n - 1; i >= 1; i-- { - t.nodes[i] = t.nodeHasher.HashNode(t.nodes[2*i], t.nodes[2*i+1]) - } + copy(t.nodes[n:], leaves) + t.buildInternalNodes() return nil } +// buildInternalNodes hashes internal nodes bottom-up, fanning out across +// goroutines once a level is wide enough. Within a level all nodes are +// independent; only the level-to-level walk is serial. +func (t *Tree) buildInternalNodes() { + // At each iteration 'start' is the index of the leftmost node on the + // current level and 'start' nodes live on the level (its width). + for start := t.nLeaves >> 1; start >= 1; start >>= 1 { + parallel.ExecuteWithThreshold(start, parallelLevelThreshold, func(lo, hi int) { + for i := start + lo; i < start+hi; i++ { + t.nodes[i] = t.nodeHasher.HashNode(t.nodes[2*i], t.nodes[2*i+1]) + } + }) + } +} + // Root returns the Merkle root digest. Build must be called first. func (t *Tree) Root() hash.Digest { return t.nodes[1] diff --git a/internal/parallel/parallel.go b/internal/parallel/parallel.go index 07ca7f3..669ad80 100644 --- a/internal/parallel/parallel.go +++ b/internal/parallel/parallel.go @@ -18,6 +18,20 @@ import ( "sync" ) +// ExecuteWithThreshold runs work in parallel when nbIterations >= threshold, +// otherwise invokes it once in the current goroutine. Saves the scheduler +// round-trip when nbIterations is tiny. +func ExecuteWithThreshold(nbIterations, threshold int, work func(int, int)) { + if nbIterations <= 0 { + return + } + if nbIterations < threshold { + work(0, nbIterations) + return + } + Execute(nbIterations, work) +} + func Execute(nbIterations int, work func(int, int), maxCpus ...int) { nbTasks := runtime.NumCPU() diff --git a/internal/poly/compute_quotient.go b/internal/poly/compute_quotient.go index 3801008..e3add9e 100644 --- a/internal/poly/compute_quotient.go +++ b/internal/poly/compute_quotient.go @@ -24,8 +24,13 @@ import ( "github.com/consensys/loom/expr" "github.com/consensys/loom/field" "github.com/consensys/loom/internal/dag" + "github.com/consensys/loom/internal/parallel" ) +// quotientParallelThreshold is the smallest size (in koalabear ops) for +// which fan-out across goroutines is worth the scheduler overhead. +const quotientParallelThreshold = 1 << 12 + // QuotientConfig configures quotient computation. type QuotientConfig struct { DomainCache *DomainCache @@ -137,20 +142,24 @@ func ComputeQuotient(Pi map[string]Polynomial, vanishingRelation dag.DAG, N int, // at this stage, all polynomials in PiCopies are in Lagrange form. We write them in canonical basis, shifted by twiddles[i][0] // to prepare the FFT on the cosets (twiddles[i][0] is ), used to avoid the zeroes on X^N-1). - for _, pCopy := range piNonConst { - // shift coset manually - smallDomain.FFTInverse(pCopy, fft.DIF) // pCopy: bit reversed, canonical - scaleByTwiddles(pCopy, twiddleFrMultiplicativeGen) // pCopy: scaled by to avoid zeroes of X^N-1 - } + parallel.Execute(len(piNonConst), func(start, end int) { + for k := start; k < end; k++ { + pCopy := piNonConst[k] + smallDomain.FFTInverse(pCopy, fft.DIF) + scaleByTwiddles(pCopy, twiddleFrMultiplicativeGen) + } + }) // at this stage, all the polynomials c[i] in c are in canonical, bit reverse, scaled by for i := range rho { // evaluate the polys shifted by on -> the result is the polys // evaluated on the coset bigDomain.FrMultiplicativeGen* - for _, pCopy := range piNonConst { - smallDomain.FFT(pCopy, fft.DIT) // pCopy: evaluated on coset i - } + parallel.Execute(len(piNonConst), func(start, end int) { + for k := start; k < end; k++ { + smallDomain.FFT(piNonConst[k], fft.DIT) + } + }) // at this stage, the polys are evaluated on bigDomain.FrMultiplicativeGen*. We can compute the rho-ith // component of the numerator @@ -160,10 +169,13 @@ func ComputeQuotient(Pi map[string]Polynomial, vanishingRelation dag.DAG, N int, } // FFTInv on piNonConst -> polys become canonical again, k-th coeff shifted by bigDomain.FrMultiplicativeGen^k* - for _, pCopy := range piNonConst { - smallDomain.FFTInverse(pCopy, fft.DIF) // pCopy: bit reversed, canonical - scaleByTwiddles(pCopy, twiddleGeneratorBigDomain) // k-th coeff now shifted by bigDomain.FrMultiplicativeGen^k* - } + parallel.Execute(len(piNonConst), func(start, end int) { + for k := start; k < end; k++ { + pCopy := piNonConst[k] + smallDomain.FFTInverse(pCopy, fft.DIF) + scaleByTwiddles(pCopy, twiddleGeneratorBigDomain) + } + }) } // X^N-1 evaluated at coset representative FrGen·bigDomain.Generator^i: @@ -189,9 +201,11 @@ func ComputeQuotient(Pi map[string]Polynomial, vanishingRelation dag.DAG, N int, for i := range rho { xnMinusOneInv[i].Inverse(&xnMinusOne[i]) } - for i := range bigSize { - numerator[i].Mul(&numerator[i], &xnMinusOneInv[i%rho]) - } + parallel.ExecuteWithThreshold(bigSize, quotientParallelThreshold, func(start, end int) { + for i := start; i < end; i++ { + numerator[i].Mul(&numerator[i], &xnMinusOneInv[i%rho]) + } + }) return numerator, nil } @@ -313,38 +327,54 @@ func ComputeQuotientMixed(PiBase map[string]Polynomial, PiExt map[string]ExtPoly } } - for _, pCopy := range baseNonConst { - smallDomain.FFTInverse(pCopy, fft.DIF) - scaleBaseByTwiddles(pCopy, twiddleFrMultiplicativeGen) - } - for _, pCopy := range extNonConst { - smallDomain.FFTInverseExt(pCopy, fft.DIF) - scaleExtByTwiddles(pCopy, twiddleFrMultiplicativeGen) - } + parallel.Execute(len(baseNonConst), func(start, end int) { + for k := start; k < end; k++ { + pCopy := baseNonConst[k] + smallDomain.FFTInverse(pCopy, fft.DIF) + scaleBaseByTwiddles(pCopy, twiddleFrMultiplicativeGen) + } + }) + parallel.Execute(len(extNonConst), func(start, end int) { + for k := start; k < end; k++ { + pCopy := extNonConst[k] + smallDomain.FFTInverseExt(pCopy, fft.DIF) + scaleExtByTwiddles(pCopy, twiddleFrMultiplicativeGen) + } + }) vals := make([]ext.E4, N) var evalWorkspace dag.EvalWorkspace for i := range rho { - for _, pCopy := range baseNonConst { - smallDomain.FFT(pCopy, fft.DIT) - } - for _, pCopy := range extNonConst { - smallDomain.FFTExt(pCopy, fft.DIT) - } + parallel.Execute(len(baseNonConst), func(start, end int) { + for k := start; k < end; k++ { + smallDomain.FFT(baseNonConst[k], fft.DIT) + } + }) + parallel.Execute(len(extNonConst), func(start, end int) { + for k := start; k < end; k++ { + smallDomain.FFTExt(extNonConst[k], fft.DIT) + } + }) vanishingRelation.EvalOnAllEntriesMixedInto(vals, _PiBase, _PiExt, N, &evalWorkspace) for j := 0; j < N; j++ { numerator[rho*j+i] = vals[j] } - for _, pCopy := range baseNonConst { - smallDomain.FFTInverse(pCopy, fft.DIF) - scaleBaseByTwiddles(pCopy, twiddleGeneratorBigDomain) - } - for _, pCopy := range extNonConst { - smallDomain.FFTInverseExt(pCopy, fft.DIF) - scaleExtByTwiddles(pCopy, twiddleGeneratorBigDomain) - } + parallel.Execute(len(baseNonConst), func(start, end int) { + for k := start; k < end; k++ { + pCopy := baseNonConst[k] + smallDomain.FFTInverse(pCopy, fft.DIF) + scaleBaseByTwiddles(pCopy, twiddleGeneratorBigDomain) + } + }) + parallel.Execute(len(extNonConst), func(start, end int) { + for k := start; k < end; k++ { + pCopy := extNonConst[k] + smallDomain.FFTInverseExt(pCopy, fft.DIF) + scaleExtByTwiddles(pCopy, twiddleGeneratorBigDomain) + } + }) } xnMinusOne := make([]koalabear.Element, rho) @@ -364,9 +394,11 @@ func ComputeQuotientMixed(PiBase map[string]Polynomial, PiExt map[string]ExtPoly for i := range rho { xnMinusOneInv[i].Inverse(&xnMinusOne[i]) } - for i := range bigSize { - numerator[i].MulByElement(&numerator[i], &xnMinusOneInv[i%rho]) - } + parallel.ExecuteWithThreshold(bigSize, quotientParallelThreshold, func(start, end int) { + for i := start; i < end; i++ { + numerator[i].MulByElement(&numerator[i], &xnMinusOneInv[i%rho]) + } + }) return numerator, nil } @@ -400,13 +432,17 @@ func CosetLagrangeNormalToCanonicalWithCache(p Polynomial, cache *DomainCache) { utils.BitReverse(p) // → coset-canonical Normal // Multiply by invFrGen^k to get standard canonical coefficients c_k. + // The serial acc dependency is broken per chunk by seeding acc with + // invFrGen^chunkStart. invFrGen := d.FrMultiplicativeGen invFrGen.Inverse(&invFrGen) - acc := koalabear.One() - for k := range p { - p[k].Mul(&p[k], &acc) - acc.Mul(&acc, &invFrGen) - } + parallel.ExecuteWithThreshold(len(p), quotientParallelThreshold, func(start, end int) { + acc := PowUint64(invFrGen, uint64(start)) + for k := start; k < end; k++ { + p[k].Mul(&p[k], &acc) + acc.Mul(&acc, &invFrGen) + } + }) } // CosetExtLagrangeToLagrangeNormal converts an extension polynomial from @@ -433,9 +469,11 @@ func CosetExtLagrangeNormalToCanonicalWithCache(p ExtPolynomial, cache *DomainCa invFrGen := d.FrMultiplicativeGen invFrGen.Inverse(&invFrGen) - acc := koalabear.One() - for k := range p { - p[k].MulByElement(&p[k], &acc) - acc.Mul(&acc, &invFrGen) - } + parallel.ExecuteWithThreshold(len(p), quotientParallelThreshold, func(start, end int) { + acc := PowUint64(invFrGen, uint64(start)) + for k := start; k < end; k++ { + p[k].MulByElement(&p[k], &acc) + acc.Mul(&acc, &invFrGen) + } + }) } diff --git a/internal/poly/utils.go b/internal/poly/utils.go index 4506f79..43c12eb 100644 --- a/internal/poly/utils.go +++ b/internal/poly/utils.go @@ -26,6 +26,23 @@ func isPowerOfTwo(n int) bool { return n > 0 && (n&(n-1)) == 0 } +// 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 + res.SetOne() + b := base + for exp != 0 { + if exp&1 == 1 { + res.Mul(&res, &b) + } + b.Mul(&b, &b) + exp >>= 1 + } + return res +} + // NextPowerOfTwo returns the next power of two greater than or equal to n func NextPowerOfTwo(n int) int { if n <= 0 { diff --git a/prover/prover.go b/prover/prover.go index c4e505c..155590a 100644 --- a/prover/prover.go +++ b/prover/prover.go @@ -17,6 +17,7 @@ import ( "fmt" "sort" "sync" + "time" "github.com/consensys/gnark-crypto/field/koalabear" ext "github.com/consensys/gnark-crypto/field/koalabear/extensions" @@ -41,9 +42,10 @@ import ( ) type Config struct { - EmulateFS bool - SkipFRI bool - HashBackend commitment.HashBackend + EmulateFS bool + SkipFRI bool + HashBackend commitment.HashBackend + PhaseCallback func(name string, d time.Duration) } type Option func(c *Config) error @@ -69,6 +71,17 @@ func WithHashBackend(backend commitment.HashBackend) Option { } } +// WithPhaseCallback installs a callback that fires after each major prover +// phase with the phase name and wall-clock duration. Phases reported (in +// order): "execute-steps", "compute-air-quotients", "evaluations-at-zeta", +// "deep-quotient+fri-commit", "fri-query-open". +func WithPhaseCallback(cb func(name string, d time.Duration)) Option { + return func(c *Config) error { + c.PhaseCallback = cb + return nil + } +} + type proverRuntime struct { friParams fri.Params Proof proof.Proof @@ -899,33 +912,49 @@ func Prove(t trace.Trace, provingKey setup.ProvingKey, publicInputs public.Input return proof.Proof{}, err } + report := func(name string, d time.Duration) { + if config.PhaseCallback != nil { + config.PhaseCallback(name, d) + } + } + // run ExecuteSteps + t0 := time.Now() if err := pr.ExecuteSteps(); err != nil { return proof.Proof{}, err } + report("execute-steps", time.Since(t0)) // run ComputeAIRQuotients + t0 = time.Now() if err := pr.ComputeAIRQuotients(); err != nil { return proof.Proof{}, err } + report("compute-air-quotients", time.Since(t0)) // run ComputeEvaluationsAtZeta + t0 = time.Now() if err := pr.ComputeEvaluationsAtZeta(); err != nil { return proof.Proof{}, err } + report("evaluations-at-zeta", time.Since(t0)) // ------ PCS related verification ------ if !config.SkipFRI { // Compute DEEP quotient and FRI-prove that it is the evaluation of a polynomial of degree N + t0 = time.Now() if err := pr.ComputeDeepQuotient(); err != nil { return proof.Proof{}, err } + report("deep-quotient+fri-commit", time.Since(t0)) // Brige FRI <-> polynomial commitments, using sample at queryPositions + t0 = time.Now() if err := pr.SampleEvaluations(); err != nil { return proof.Proof{}, err } + report("fri-query-open", time.Since(t0)) } return pr.Proof, nil