Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions field/babybear/extensions/e4_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 57 additions & 0 deletions field/babybear/extensions/vector.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions field/generator/internal/templates/extensions/e4_test.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"math/big"
"reflect"
"sort"
"runtime"
"bytes"

fr "{{ .FieldPackagePath }}"
Expand Down Expand Up @@ -600,6 +601,49 @@ func TestVectorExp(t *testing.T) {

}

// prefixProductGeneric computes the prefix product of the vector in place (single-threaded).
func prefixProductGeneric(vector Vector) {
if len(vector) == 0 {
return
}
for i := 1; i < len(vector); i++ {
vector[i].Mul(&vector[i-1], &vector[i])
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The multiplication order is incorrect. This should be vector[i].Mul(&vector[i], &vector[i-1]) to match the prefix product definition where each element becomes the product of itself with all previous elements.

Suggested change
vector[i].Mul(&vector[i-1], &vector[i])
vector[i].Mul(&vector[i], &vector[i-1])

Copilot uses AI. Check for mistakes.
}
}

func randomVector(size int) Vector {
v := make(Vector, size)
for i := range v {
v[i].MustSetRandom()
}
return v
}

func TestPrefixProduct_EmptyVector(t *testing.T) {
assert := require.New(t)
v := make(Vector, 0)
expected := make(Vector, 0)
prefixProductGeneric(expected)
v.PrefixProduct()
assert.Equal(expected, v)
}

func TestPrefixProduct_VariousNbTasks(t *testing.T) {
assert := require.New(t)
sizes := []int{1, 2, 256, 1024}
nbTasksList := []int{1, 16, 32, runtime.NumCPU()}
for _, size := range sizes {
for _, nbTasks := range nbTasksList {
v := randomVector(size)
expected := make(Vector, size)
copy(expected, v)
prefixProductGeneric(expected)
v.PrefixProduct(nbTasks)
assert.Equal(expected, v, "size=%d nbTasks=%d", size, nbTasks)
}
}
}


func TestVectorEmptyOps(t *testing.T) {
assert := require.New(t)
Expand Down Expand Up @@ -869,6 +913,28 @@ func BenchmarkVectorOps(b *testing.B) {
}
}

func BenchmarkPrefixProduct(b *testing.B) {
const N = 1 << 19
a1 := make(Vector, N)
for i := 0; i < N; i++ {
a1[i].MustSetRandom()
}

b.Run("generic", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
prefixProductGeneric(a1)
}
})

b.Run("PrefixProduct", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
a1.PrefixProduct()
}
})

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: In-place Modification Corrupts Benchmark Data

The BenchmarkPrefixProduct reuses the same a1 vector across sub-benchmarks and iterations. Since prefixProductGeneric and PrefixProduct modify a1 in-place, subsequent benchmark runs operate on already-modified data, leading to corrupted input and unreliable performance measurements.

Additional Locations (1)

Fix in Cursor Fix in Web



func BenchmarkVectorSerialization(b *testing.B) {
Expand Down
58 changes: 58 additions & 0 deletions field/generator/internal/templates/extensions/vector.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"bytes"
"slices"
"encoding/binary"
"runtime"
"github.com/consensys/gnark-crypto/internal/parallel"

fr "{{ .FieldPackagePath }}"
{{- if .IsKoalaBear}}
Expand Down Expand Up @@ -537,6 +539,62 @@ func (vector *Vector) ReadFrom(r io.Reader) (int64, error) {
return n, <-chErr
}

// PrefixProduct computes the prefix product of the vector in place.
// i.e. vector[i] = vector[0] * vector[1] * ... * vector[i]
// If nbTasks > 1, it uses nbTasks goroutines to compute the prefix product in parallel.
// If nbTasks is not provided, it uses the number of CPU cores.
func (vector Vector) PrefixProduct(nbTasks ...int) {
N := len(vector)
if N < 2 {
return
}

if N < 512 {
vector.prefixProductGeneric()
return
}

// Use one worker per available CPU core.
numWorkers := runtime.GOMAXPROCS(0)
if len(nbTasks) == 1 && nbTasks[0] > 0 && nbTasks[0] < numWorkers {
numWorkers = nbTasks[0]
}

for N / numWorkers < 64 && numWorkers > 1 {
numWorkers >>= 1
}
numWorkers = max(1, numWorkers)

// --- PASS 1: Calculate prefix product for each chunk independently ---
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: this comment has an extra tab compared to the surrounding code. It should align with the parallel.Execute call below it.

Suggested change
// --- PASS 1: Calculate prefix product for each chunk independently ---
// --- PASS 1: Calculate prefix product for each chunk independently ---

Copilot uses AI. Check for mistakes.
parallel.Execute(N, func(start, stop int) {
// This is the original sequential algorithm applied to the smaller chunk.
for j := start + 1; j < stop; j++ {
vector[j].Mul(&vector[j], &vector[j-1])
}
}, numWorkers)

chunks := parallel.Chunks(N, numWorkers)

// the first chunk is correct, we need to update the other chunks
for i := 1; i < len(chunks); i++ {
// take last value from previous chunk
prevChunkEnd := chunks[i-1][1] - 1
multiplier := vector[prevChunkEnd]
// multiply all values in current chunk by this value
start, stop := chunks[i][0], chunks[i][1]
subVector := vector[start:stop]
subVector.ScalarMul(subVector, &multiplier)
}

}

func (vector Vector) prefixProductGeneric() {
for i := 1; i < len(vector); i++ {
vector[i].Mul(&vector[i], &vector[i-1])
}
}



func vectorAddGeneric(res, a, b Vector) {
for i := 0; i < len(res); i++ {
Expand Down
Loading
Loading