Skip to content

perf(ecc): price the batch-affine chunk processor in the MultiExp cost model - #875

Open
4waan wants to merge 4 commits into
Consensys-Incorporated:masterfrom
4waan:perf/multiexp-window-tie-break
Open

perf(ecc): price the batch-affine chunk processor in the MultiExp cost model#875
4waan wants to merge 4 commits into
Consensys-Incorporated:masterfrom
4waan:perf/multiexp-window-tie-break

Conversation

@4waan

@4waan 4waan commented Aug 21, 2026

Copy link
Copy Markdown

Description

MultiExp pippenger window counts bucket accumulation as when c<=9 it uses sequential extended jacobian while c>=10 accumulates with `processChunk * Batchffine* which ends a single field inversion over a whole batch

BLS12-381 G1, _innerMsmG1 at fixed c so the split heuristic is out of the picture, on apple M2:

    n      c=9      c=10
  1024   3.52ms   3.61ms
  1536   4.78ms   4.68ms   <- real crossover
  2048   5.80ms   5.25ms    -9.5%
  4096  11.02ms   8.49ms     -23%

The 9 -> 10 boundary belongs near n=1300, not 4096.

What this changes

Mean distance from the per-size optimum, timing _innerMsm at fixed c over BLS12-381 and BN254 G1, nbPoints 1024..32768, c 8..13:

model serial at NbTasks 8 and 16
master 1.0276 1.0422
flat w = 0.76 1.0118 1.0072
this PR 1.0037 1.0069

Window boundaries, BLS12-381 G1:

master  768->c8  1793->c9  4097->c10   9217->c11  20481->c12  45057->c13
this PR 768->c8            1225->c10   6179->c11  24095->c12  48448->c13

Every one of those matches the measured optimum. c=9 is no longer selected: it never wins against both c=8 and c=10.

Benchmarking

BLS12-381 G1 MultiExp, master vs this branch, M2 8-core, min of 5 interleaved rounds against a master worktree.

      n     NbTasks=1   NbTasks=8   default(16)   NbTasks=32
    300         +1.6%       -3.1%         +5.4%        -9.7%
    700         -0.9%       +1.3%         -7.7%        -6.5%
   1536         -2.4%      -18.9%         -9.3%        -4.9%
   2500        -13.7%       -8.7%        -13.8%       -14.9%
   4096        -17.3%      -22.8%        -20.0%       -16.8%
   8192         -2.4%       -5.6%         -2.8%        +2.7%
  12358         -4.1%       +4.9%         -2.2%        -2.8%
  20000         +1.2%       -3.0%         -0.5%        -4.4%
 100000         -3.1%       -6.0%         -3.4%        -0.8%

Worst delta across 36 configurations is +5.4%, at a sub-millisecond size where the same n is -9.7% at NbTasks=32. The n=8192 / NbTasks=8 regression noted on the previous revision is gone (-5.6%).

[notes by llm]

  • The host was under variable load during the session (load average 5 to 38 on 8 cores). Every number above is min-of-N and interleaved A/B, which I trust directionally, but the table deserves a re-run on a quiet machine before merge.
  • bw6-761 and bw6-633 have a different window set ({4,5,8,10,16}) and inherit constants fitted on BLS12-381 and BN254. The fit is flat and the boundaries are independent of fr.Bits, but this is an extrapolation and is not directly measured.
  • The split heuristic still needs its own PR. On this host it never pays off, master included: at NbTasks >= NumCPU splitting cannot add parallelism, only work, and every split I could measure loses (from +5.6% at n=200000 to +21.3% at n=1540). This PR only makes the cost model self-consistent and stops it over-splitting; it does not redesign the heuristic. Happy to open that follow-up.

Checklist:

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (n/a, no user-facing docs affected)
  • I have added tests that prove my fix is effective or that my feature works
  • I did not modify files generated from templates. The edits are in internal/generator/ecc/generate.go and the multiexp*.go.tmpl templates; the ecc/*/multiexp*.go in this diff are the output of go generate ./internal/generator/...
  • golangci-lint does not output errors locally (0 issues)
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules (n/a)

4waan and others added 2 commits August 21, 2026 17:28
MultiExp picks the pippenger window size c by minimising estimated
group operation count:

    `cost(c) = (fr.Bits + 1) * (nbPoints + 2^c) / c`

The comparison was strict and the loop ascends over the implemented
window sizes, so whenever two window sizes score identically the
smaller one won. Setting cost(c) = cost(c+1) makes the (fr.Bits + 1)
factor cancel, and for a contiguous window set the model is exactly
indifferent at nbPoints = 2^c * (c - 1).

Windows c <= 9 use the extended Jacobian chunk processor, while
c >= 10 use the affine one to kill single field inversion over
the bunch of bucket additions.
Inversion costs roughly 84 muls spreading it across 80 to 640
bucket additions -> affine addition..cheaper than the Jacobian mixed adds
The cost model counts a bucket accumulation and a bucket reduction as
one group operation each and cannot see any of this

Exactly one tie per curve crosses the Jacobian/batch-affine boundary->

    n = 4096   c 9 -> 10    bn254, bls12-377, bls12-381, bls24-315,
                            bls24-317, grumpkin, stark-curve,
                            secp256k1, secp256r1
    n = 2816   c 8 -> 10    bw6-761
    n = 7424   c 8 -> 12    bw6-633

4096 is ScalarsPerBlob, so the eip4844 commitment and proof paths sat
exactly on the wrong side of this tie.

Measured on my apple M2, BLS12-381 G1, min of 5 runs:

    n=4095  tasks=1   76.73 ms -> 76.66 ms   (control, c unchanged)
    n=4096  tasks=1   76.65 ms -> 64.76 ms   -15.5%
    n=4097  tasks=1   64.74 ms -> 64.73 ms   (control, c unchanged)
    n=4096  tasks=16  13.12 ms -> 10.27 ms   -21.7%
Extract bestC from MultiExp into a package-level bestCG1/bestCG2 so the window
selection is reachable from a test, then assert it returns the largest cost-minimising window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@4waan
4waan marked this pull request as draft August 21, 2026 13:07
Only one tie per curve crosses the Jacobian/batch-affine boundary, not all.
Comment thread internal/generator/ecc/template/multiexp.go.tmpl Outdated
Comment thread internal/generator/ecc/template/multiexp.go.tmpl
@4waan
4waan marked this pull request as ready for review August 22, 2026 08:01
…t model

Weight a bucket accumulation by the batch its inversion amortises over, use the same model for the split probe, and give costFunction a real CPU count.
@4waan 4waan changed the title Perf/multiexp window tie break perf(ecc): price the batch-affine chunk processor in the MultiExp cost model Aug 22, 2026
@4waan

4waan commented Aug 31, 2026

Copy link
Copy Markdown
Author

@gbotrel , a review would be great for me to push ahead my work! could you approve CI please?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants