Skip to content

perf(backends/arkworks): batch normalization + rayon-parallel DoryRoutines - #25

Closed
0xAndoroid wants to merge 3 commits into
mainfrom
perf/arkworks-parallel-routines
Closed

perf(backends/arkworks): batch normalization + rayon-parallel DoryRoutines#25
0xAndoroid wants to merge 3 commits into
mainfrom
perf/arkworks-parallel-routines

Conversation

@0xAndoroid

Copy link
Copy Markdown
Collaborator

Ports the optimized Dory routines that Jolt runs in production (JoltG1Routines/JoltG2Routines from a16z/jolt#1714) upstream into the stock backends::arkworks G1Routines/G2Routines, so every dory-pcs consumer gets them.

What changed

  • msm (G1 + G2): when bases are not already normalized, convert to affine with one batched inversion (normalize_batch) instead of one field inversion per point. Already-normalized bases (z = one — i.e. setup generators, which back the commit-path row MSMs and the first-round e_beta MSMs) keep the inversion-free per-element into_affine path; a two-comparison-per-point z scan dispatches. Folded vectors mid reduce-round have arbitrary z and take the batch path — that's where the win is.
  • fixed_base_vector_scalar_mul, fixed_scalar_mul_bases_then_add, fixed_scalar_mul_vs_then_add, fold_field_vectors (G1 + G2): per-element loops parallelized with rayon under the existing parallel feature (same #[cfg] pattern as ark_pairing.rs), serial fallback unchanged. G1/G2 bodies share private generic helpers.
  • Safe wrapper conversions (separate commit): into_inner() + From impls in both directions for ArkFrFr, ArkG1G1Projective, ArkG2G2Projective, so downstream consumers don't need transmutes for the common cases.

Deliberately not ported: Jolt's GLV kernels (jolt_optimizations::*, e.g. vector_add_scalar_mul_g1_online). They live in the a16z/arkworks-algebra fork, and dory-pcs publishes to crates.io (no git deps), so those call sites became plain rayon-parallel arkworks scalar muls. They remain a further ~speedup available downstream in Jolt.

Correctness

Group results are exact, so commitments, proofs, and transcripts are byte-identical to the stock routines. New unit tests pin every routine to a naive scale/add reference for G1 and G2, covering identity points, zero scalars, empty inputs, and both msm dispatch paths (mixed-z and all-normalized bases). Full suites pass on both CI feature matrices (backends,parallel,cache,disk-persistence and +zk), run single-threaded locally: 76 + 133 tests.

Heads-up (unrelated, pre-existing): running the suite multi-process can flake tests that call setup::<BN254>(10) — that's the disk-persisted setup race #24, reproduced on stock main.

Benchmarks

cargo bench --bench arkworks_proof --features backends,cache,parallel -- --quick (official 2^26 size, nu = sigma = 13), Apple M4 (10 cores, 16 GB), quiet machine, back-to-back runs, fresh criterion baseline:

bench main (56a7243) this PR change
prove_2^26_coefficients 12.13 s 9.06 s −25%
verify_2^26_coefficients 73.3 ms 65.5 ms ~unchanged¹
single commit call at 2^26 (2 iters, one-shot harness) 87–95 s 75–76 s ~unchanged¹

¹ Within this laptop's run-to-run variance band. Expected: the verifier never calls DoryRoutines (its checks are pairings/GT ops), and commit-path MSM bases are already-normalized setup generators, where the dispatch keeps the byte-identical stock path. An earlier draft normalized unconditionally and regressed commit ~1.7× at 2^26 — that's what the z-scan dispatch (third commit) is for.

The prove win comes from batch normalization of the folded (arbitrary-z) round vectors plus parallel G1/G2 vector folds; it grows with core count (Jolt measured larger wins on many-core machines with the same structure, on top of its fork-only GLV kernels).

Numbers were taken with criterion --quick on a fanless laptop — treat them as directional; happy to rerun anything.

…tines

msm() converts bases to affine with a single batched inversion
(normalize_batch) instead of one field inversion per point, for both G1
and G2. fixed_base_vector_scalar_mul, fixed_scalar_mul_bases_then_add,
fixed_scalar_mul_vs_then_add, and fold_field_vectors run their
per-element loops in parallel under the existing `parallel` feature,
falling back to the previous serial loops otherwise.

Group results are exact, so commitments, proofs, and transcripts are
byte-identical to the previous routines. Unit tests pin every routine
to a naive reference, including identity-point / zero-scalar edge cases.

Ported from a16z/jolt#1714 (JoltG1Routines/JoltG2Routines).
into_inner() plus From impls in both directions between the wrappers
and their inner arkworks types, so downstream consumers no longer need
transmutes (the wrappers are repr(transparent), but per-element
conversions cover the common cases safely).
…bases

Batch normalization is only a win when bases actually need an inversion.
Dory's setup generators (and hence the commit-path row MSMs and the
first-round e_beta MSMs, whose bases are setup slices) are already
affine-normalized (z = one), where into_affine short-circuits without
any inversion and normalize_batch pays ~6 field muls per point plus a
slice copy for nothing — a measurable commit regression at 2^26.

msm now scans z values (two field comparisons per point) and dispatches:
all z in {0, 1} -> per-element into_affine (no inversions at all);
otherwise -> one batched inversion for the whole slice. Folded vectors
mid reduce-round have arbitrary z and keep the batch path.
@moodlezoup

Copy link
Copy Markdown
Collaborator

superseded by #27

@moodlezoup moodlezoup closed this Aug 5, 2026
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