Files
linea-monorepo/prover/protocol/compiler/globalcs/merging.go
AlexandreBelling 7334693931 Prover(perf): faster global constraints compilation (#704)
* bench(global): adds a benchmark for the global constraint compiler

* perf(merging): accumulates the factors before creating the expression

* perf(product): computes the ESH without using a smart-vector

* perf(factor): preallocations in the factorization algorithm

* perf(removeZeroes): implements a lazy allocation mechanism in removeZeroCoeffs

* perfs(alloc): counts the ret elements before returning in expandTerms to minimze the number of allocations.

* perf(factor): use an integer map instead of a field.Element map when possible

* fixup(expands): fix the skip condition for term expansion

* perf(constructor): improves the immutable constructors to reduce the number of calls to NewProduct and NewLinComb

* feat(repr): adds a json repr function to help debugging

* test(constructor): cleans the test of the constructors

* perf(factor): address maps using the first limb of a field.Element instead of the full field.Element

* fixup(commit): adds missing file in previous commit

* perf(factor): reduce the number of calls to rankChildren

* perf(rmpolyeval): creates the equivalent expression more directly to save on unnecessary optims

* perf(factors): use a counter in getCommonProdParentOfCs

* perf(factor): remove map copy from findGdChildrenGroup and replace getCommonProdParent by a simpler function

* clean(factor): remove unneeded function and imports

* feat(utils): adds a generic sort interface implementation

* perf(rankChildren): lazy allocation of the map to save on allocations

* perf(factorize): reduces the loop-bound for factorizeExpression

* (chore): fix a missing argument and format gofmt

* feat: readd test

---------

Signed-off-by: AlexandreBelling <alexandrebelling8@gmail.com>
Co-authored-by: gusiri <dreamerty@postech.ac.kr>
2025-03-21 20:55:54 +09:00

233 lines
8.0 KiB
Go

package globalcs
import (
"math/big"
"reflect"
"github.com/consensys/linea-monorepo/prover/maths/fft"
"github.com/consensys/linea-monorepo/prover/maths/field"
"github.com/consensys/linea-monorepo/prover/protocol/coin"
"github.com/consensys/linea-monorepo/prover/protocol/ifaces"
"github.com/consensys/linea-monorepo/prover/protocol/query"
"github.com/consensys/linea-monorepo/prover/protocol/variables"
"github.com/consensys/linea-monorepo/prover/protocol/wizard"
"github.com/consensys/linea-monorepo/prover/protocol/wizardutils"
"github.com/consensys/linea-monorepo/prover/symbolic"
"github.com/consensys/linea-monorepo/prover/utils"
)
// mergingCtx collects all the compilation input, output and artefacts pertaining
// the merging of the global constraints
type mergingCtx struct {
// DomainSize corresponds to the shared domain size of all the uncompiled global
// constraints. The fact that all the global constraints share the same domain
// size is a pre-condition for the compiler and is enforced by Split/Stick compilers.
DomainSize int
// RatioBuckets stores arranged by "ratio".
//
// The "ratio" denotes the ratio
// between the quotient size for that particular constraint and the domain
// size of the constraint.
//
//
// All the constraints happening here have the same domain size even when they
// are in different bucket. The only difference is then the degree and the
// and the offset of the constraint.
//
// The constraints that are stored here are modified w.r.t. the one initially
// present in the compiledIOP when the compiler starts because they are multiplied
// by X-1, X-\omega, X-\omega^2 to account for the bound cancellation.
RatioBuckets map[int][]*symbolic.Expression
// Ratios stores the list of the "ratios" in the order in which they are
// encountered in the compiled IOP.
//
// The "ratio" denotes the ratio
// between the quotient size for that particular constraint and the domain
// size of the constraint.
Ratios []int
}
// accumulateConstraints scans comp to collect uncompiled global constraints and
// aggregate them into unified global constraints per "ratio".
//
// See [mergingCtx.Ratios] for an explanation for "ratio".
func accumulateConstraints(comp *wizard.CompiledIOP) (mergingCtx, bool) {
ctx := mergingCtx{
RatioBuckets: make(map[int][]*symbolic.Expression),
}
for _, qName := range comp.QueriesNoParams.AllUnignoredKeys() {
// Filter only the global constraints
cs, ok := comp.QueriesNoParams.Data(qName).(query.GlobalConstraint)
if !ok {
// Not a global constraint
continue
}
// For the first iteration, the domain size is unset so we need to initialize
// it. This works because the domain size of a constraint cannot legally
// be 0.
if ctx.DomainSize == 0 {
ctx.DomainSize = cs.DomainSize
}
// This enforces the precondition that all the global constraint must
// share the same domain.
if cs.DomainSize != ctx.DomainSize {
utils.Panic("At this point in the compilation process, we expect all constraints to have the same domain")
}
// Mark the constraint as ignored, so that it does not get compiled a
// second time by a sub-sequent round of compilation.
comp.QueriesNoParams.MarkAsIgnored(qName)
ctx.registerCs(cs)
}
if ctx.DomainSize == 0 {
// There is no global constraint to compile
return mergingCtx{}, false
}
return ctx, true
}
// aggregateConstraints returns the list of the aggregated constraints
func (ctx *mergingCtx) aggregateConstraints(comp *wizard.CompiledIOP) []*symbolic.Expression {
var (
aggregateExpressions = make([]*symbolic.Expression, len(ctx.Ratios))
initialRound = comp.NumRounds()
mergingCoin = comp.InsertCoin(initialRound, coin.Name(deriveName(comp, DEGREE_RANDOMNESS)), coin.Field)
)
for i, ratio := range ctx.Ratios {
aggregateExpressions[i] = symbolic.NewPolyEval(mergingCoin.AsVariable(), ctx.RatioBuckets[ratio])
}
return aggregateExpressions
}
// registerCs determines the ratio of a constraint and appends it to the corresponding
// bucket.
func (ctx *mergingCtx) registerCs(cs query.GlobalConstraint) {
var (
bndCancelledExpr = getBoundCancelledExpression(cs)
ratio = getExprRatio(bndCancelledExpr)
)
// Initialize the outer-maps / slices if the entries are not already allocated
if _, ok := ctx.RatioBuckets[ratio]; !ok {
ctx.RatioBuckets[ratio] = []*symbolic.Expression{}
ctx.Ratios = append(ctx.Ratios, ratio)
}
ctx.RatioBuckets[ratio] = append(ctx.RatioBuckets[ratio], bndCancelledExpr)
}
// getBoundCancelledExpression computes the "bound cancelled expression" for the
// constraint cs. Namely, the constraints expression is multiplied by terms of the
// form X-\omega^k to cancel the expression at position "k" if required. If the
// constraint uses the "noBoundCancel" feature, then the constraint expression is
// directly returned.
func getBoundCancelledExpression(cs query.GlobalConstraint) *symbolic.Expression {
if cs.NoBoundCancel {
return cs.Expression
}
var (
cancelRange = cs.MinMaxOffset()
res = cs.Expression
domainSize = cs.DomainSize
x = variables.NewXVar()
omega = fft.GetOmega(domainSize)
// factors is a list of expression to multiply to obtain the return expression. It
// is initialized with "only" the initial expression and we iteratively add the
// terms (X-i) to it. At the end, we call [sym.Mul] a single time. This structure
// is important because it [sym.Mul] operates a sequence of optimization routines
// that are everytime we call it. In an earlier version, we were calling [sym.Mul]
// for every factor and this were making the function have a quadratic/cubic runtime.
factors = make([]any, 0, utils.Abs(cancelRange.Max)+utils.Abs(cancelRange.Min)+1)
)
factors = append(factors, res)
// appendFactor appends an expressions representing $X-\rho^i$ to [factors]
appendFactor := func(i int) {
var root field.Element
root.Exp(omega, big.NewInt(int64(i)))
factors = append(factors, symbolic.Sub(x, root))
}
if cancelRange.Min < 0 {
// Cancels the expression on the range [0, -cancelRange.Min)
for i := 0; i < -cancelRange.Min; i++ {
appendFactor(i)
}
}
if cancelRange.Max > 0 {
// Cancels the expression on the range (N-cancelRange.Max-1, N-1]
for i := 0; i < cancelRange.Max; i++ {
point := domainSize - i - 1 // point at which we want to cancel the constraint
appendFactor(point)
}
}
// When factors is of length 1, it means the expression does not need to be
// bound-cancelled and we can directly return the original expression
// without calling [sym.Mul].
if len(factors) == 1 {
return res
}
return symbolic.Mul(factors...)
}
// getExprRatio computes the ratio of the expression and ceil to the next power
// of two. The input expression should be pre-bound-cancelled. The domainSize
func getExprRatio(expr *symbolic.Expression) int {
var (
board = expr.Board()
domainSize = wizardutils.ExprIsOnSameLengthHandles(&board)
exprDegree = board.Degree(GetDegree(domainSize))
quotientSize = exprDegree - domainSize + 1
ratio = utils.DivCeil(quotientSize, domainSize)
)
return utils.NextPowerOfTwo(max(1, ratio))
}
// GetDegree is a generator returning a DegreeGetter that can be passed to
// [symbolic.ExpressionBoard.Degree]. The generator takes the domain size as
// input.
func GetDegree(size int) func(iface interface{}) int {
return func(iface interface{}) int {
switch v := iface.(type) {
case ifaces.Column:
// Univariate polynomials is X. We pad them with zeroes so it is safe
// to return the domainSize directly.
if size != v.Size() {
panic("unconsistent sizes for the commitments")
}
// The size gives the number of coefficients , but we return the degree
// hence the - 1
return v.Size() - 1
case coin.Info, ifaces.Accessor:
// Coins are treated
return 0
case variables.X:
return 1
case variables.PeriodicSample:
return size - size/v.T
default:
utils.Panic("Unknown type %v\n", reflect.TypeOf(v))
}
panic("unreachable")
}
}