diff --git a/crates/halo2_proofs/src/poly/commitment.rs b/crates/halo2_proofs/src/poly/commitment.rs index 287d8838..c8882ab6 100644 --- a/crates/halo2_proofs/src/poly/commitment.rs +++ b/crates/halo2_proofs/src/poly/commitment.rs @@ -1579,6 +1579,39 @@ impl Params { r_u: C::Scalar, r_w: C::Scalar, ) -> Option<(C::Curve, C::Curve)> { + #[cfg(feature = "orbits")] + { + let n = self.n as usize; + let half = n / 2; + assert_eq!(p_hi.len(), half, "one scalar per lower-half base"); + assert_eq!(p_lo.len(), half, "one scalar per upper-half base"); + if crate::multicore::current_num_threads() > prepared_commitment_max_threads(self.k) { + return None; + } + let prepared = self.zero_check()?; + if prepared.terms() != n + PREPARED_COMMITMENT_EXTRA_BASES { + return None; + } + // Reuse [g..., w, u], leaving the inactive generator half zero. + // Both round commitments retain their original scalar pairing. + return Some(crate::multicore::join( + || { + let mut scalars = vec![C::Scalar::ZERO; n + PREPARED_COMMITMENT_EXTRA_BASES]; + scalars[..half].copy_from_slice(p_hi); + scalars[n] = l_w; + scalars[n + 1] = l_u; + prepared.multiexp_with_terms_vartime(&scalars, &[]) + }, + || { + let mut scalars = vec![C::Scalar::ZERO; n + PREPARED_COMMITMENT_EXTRA_BASES]; + scalars[half..n].copy_from_slice(p_lo); + scalars[n] = r_w; + scalars[n + 1] = r_u; + prepared.multiexp_with_terms_vartime(&scalars, &[]) + }, + )); + } + #[cfg(all(feature = "multicore", not(feature = "orbits")))] { let half = self.n as usize / 2; @@ -1606,7 +1639,7 @@ impl Params { return Some((l_body + l_auxiliary, r_body + r_auxiliary)); } - #[cfg(any(not(feature = "multicore"), feature = "orbits"))] + #[cfg(not(any(feature = "multicore", feature = "orbits")))] { let _ = (p_hi, p_lo, l_u, l_w, r_u, r_w); None @@ -1641,7 +1674,7 @@ impl Params { /// effective threads. Orchard-sized (`k = 11`) tables on `AArch64` macOS /// extend that bound to ten, where end-to-end proving stays ahead on the /// benchmarked M4 system. Wider pools and unmeasured SRS shapes keep the - /// planned commitment multiexp. Without `orbits`, the first IPA round also + /// planned commitment multiexp. The first IPA round also /// reuses the coefficient table. Its two generator MSMs each have one /// active half and one zero half: the backend still recodes and scans all /// scalar slots, but zero scalars do not fetch prepared points or populate diff --git a/crates/halo2_proofs/src/poly/commitment/prover.rs b/crates/halo2_proofs/src/poly/commitment/prover.rs index 5add7e8b..b67273bf 100644 --- a/crates/halo2_proofs/src/poly/commitment/prover.rs +++ b/crates/halo2_proofs/src/poly/commitment/prover.rs @@ -343,16 +343,18 @@ pub(in crate::poly) fn create_proof_with_powers< b_scale += b_hi_scale * u_j; } - // Collapse `G'` - parallel_generator_collapse(&mut g_prime, u_j); - g_prime.truncate(half); + // The final folded generator is not used by the prover. + if half > 1 { + parallel_generator_collapse(&mut g_prime, u_j); + g_prime.truncate(half); + } // Update randomness (the synthetic blinding factor at the end) f += &(l_j_randomness * &u_j_inv); f += &(r_j_randomness * &u_j); } - // We have fully collapsed `p_prime`, `b`, `G'` + // The polynomial coefficients have fully collapsed. assert_eq!(p_prime.len(), 1); let c = p_prime[0]; @@ -392,17 +394,17 @@ fn parallel_generator_collapse(g: &mut [C], challenge: C::Scalar #[cfg(test)] mod tests { - #[cfg(all(feature = "multicore", not(feature = "orbits")))] + #[cfg(any(feature = "multicore", feature = "orbits"))] use super::create_proof; use super::{ Params, compute_ipa_hi_evaluation_pasta, ipa_masking_commitment, ipa_round_multiexp, parallel_generator_collapse, sample_ipa_masking_polynomial, }; use crate::arithmetic::{CurveAffine, best_multiexp, compute_inner_product, eval_polynomial}; - #[cfg(all(feature = "multicore", not(feature = "orbits")))] + #[cfg(feature = "multicore")] use crate::poly::commitment::prepared_commitment_max_threads; use crate::poly::{EvaluationDomain, commitment::Blind, power_vector}; - #[cfg(all(feature = "multicore", not(feature = "orbits")))] + #[cfg(any(feature = "multicore", feature = "orbits"))] use crate::transcript::{Blake2bWrite, Challenge255, Transcript, TranscriptWrite}; #[cfg(feature = "multicore")] use crate::{PREPARED_SPARSE_COMMITMENT_K, PreparedSparseCommitments}; @@ -410,7 +412,7 @@ mod tests { use group::{Curve, Group}; use pasta_curves::{pallas, vesta}; use rand::rng; - #[cfg(all(feature = "multicore", not(feature = "orbits")))] + #[cfg(any(feature = "multicore", feature = "orbits"))] use rand::{SeedableRng, rngs::StdRng}; use std::fmt::Debug; @@ -449,7 +451,7 @@ mod tests { } } - #[cfg(all(feature = "multicore", not(feature = "orbits")))] + #[cfg(any(feature = "multicore", feature = "orbits"))] fn prepared_first_round_matches_ordinary() where C: CurveAffine + core::fmt::Debug, @@ -538,7 +540,7 @@ mod tests { }); } - #[cfg(all(feature = "multicore", not(feature = "orbits")))] + #[cfg(any(feature = "multicore", feature = "orbits"))] fn prepared_first_round_preserves_opening_proof() { const K: u32 = 6; const PROOF_SEED: u64 = 0x4950_412d_524f_554e; @@ -853,19 +855,19 @@ mod tests { round_multiexp_matches_split::(); } - #[cfg(all(feature = "multicore", not(feature = "orbits")))] + #[cfg(any(feature = "multicore", feature = "orbits"))] #[test] fn prepared_first_round_matches_ordinary_pallas() { prepared_first_round_matches_ordinary::(); } - #[cfg(all(feature = "multicore", not(feature = "orbits")))] + #[cfg(any(feature = "multicore", feature = "orbits"))] #[test] fn prepared_first_round_matches_ordinary_vesta() { prepared_first_round_matches_ordinary::(); } - #[cfg(all(feature = "multicore", not(feature = "orbits")))] + #[cfg(any(feature = "multicore", feature = "orbits"))] #[test] fn prepared_first_round_preserves_unprepared_opening_proof() { prepared_first_round_preserves_opening_proof(); diff --git a/crates/pasta_curves/src/glv.rs b/crates/pasta_curves/src/glv.rs index 09e46288..a996aa6e 100644 --- a/crates/pasta_curves/src/glv.rs +++ b/crates/pasta_curves/src/glv.rs @@ -1826,49 +1826,45 @@ fn multiexp_serial( } /// $\sum_{w < \text{windows}} B^w C_w$ for $C_w$ = `window_sum(w)`, with -/// adjacent windows paired per rayon task. The pair members are still -/// computed as independent (joined) subtasks — so wide pools stay as -/// occupied as with one task per window, which measured best against -/// contiguous window chunks on a 32-core x86-64 host — while the pair -/// shares one Horner shift chain, halving the doublings a -/// one-task-per-window schedule duplicates. The pair roots remain -/// independent, so their remaining shifts overlap. `None` from +/// windows combined through a balanced tree. Every window remains an +/// independent Rayon task. Each merge shifts only its upper subtree, +/// sharing the shift across all of that subtree's windows. This needs +/// O(windows log windows) doublings instead of independently shifting +/// every pair to its absolute position. `None` from /// `window_sum` (an arithmetic guard) propagates out. Shared by the -/// Eisenstein-orbit backend and the prepared zero-check's main-window and -/// tail drivers. +/// Eisenstein-orbit backend and the prepared +/// zero-check's main-window and tail drivers. #[cfg(feature = "multicore")] -fn paired_windows_sum( +fn balanced_windows_sum( windows: usize, window_bits: usize, window_sum: impl Fn(usize) -> Option + Sync, ) -> Option { - const WINDOWS_PER_PAIR: usize = 2; - let pair_count = windows.div_ceil(WINDOWS_PER_PAIR); - (0..pair_count) - .into_par_iter() - .map(|pair| { - let start = pair * WINDOWS_PER_PAIR; - let mut sum = if start + 1 == windows { - window_sum(start)? - } else { - let (low, high) = maybe_rayon::join(|| window_sum(start), || window_sum(start + 1)); - let mut low = low?; + fn sum_range( + start: usize, + windows: usize, + window_bits: usize, + window_sum: &(impl Fn(usize) -> Option + Sync), + ) -> Option { + match windows { + 0 => Some(C::identity()), + 1 => window_sum(start), + _ => { + let half = windows / 2; + let (low, high) = maybe_rayon::join( + || sum_range(start, half, window_bits, window_sum), + || sum_range(start + half, windows - half, window_bits, window_sum), + ); let mut high = high?; - for _ in 0..window_bits { + for _ in 0..window_bits * half { high = high.double(); } - low += high; - low - }; - for _ in 0..window_bits * start { - sum = sum.double(); + Some(low? + high) } - Some(sum) - }) - .try_reduce(C::identity, |mut left, right| { - left += right; - Some(left) - }) + } + } + + sum_range(0, windows, window_bits, &window_sum) } /// Evaluates each Signed-Booth window independently through Rayon, then @@ -4040,6 +4036,49 @@ mod tests { const VERIFIER_MULTIEXP_SIZES: [usize; 3] = [2_150, 2_990, 5_678]; + #[cfg(feature = "multicore")] + fn parallel_windows_match_weighted_sum() { + for windows in [0, 1, 2, 3, 5, 20, 21, 33] { + let points: Vec<_> = (0..windows) + .map(|index| match index % 5 { + 0 => C::identity(), + 1 => C::generator(), + 2 => -C::generator(), + _ => C::generator() * C::ScalarExt::from(index as u64), + }) + .collect(); + for bits in [1, 4, 7] { + let radix = C::ScalarExt::from(1 << bits); + let mut power = C::ScalarExt::ONE; + let mut expected = C::identity(); + for point in &points { + expected += *point * power; + power *= radix; + } + assert_eq!( + balanced_windows_sum::(windows, bits, |index| Some(points[index])), + Some(expected), + "windows={windows}, bits={bits}", + ); + for failure in 0..windows { + assert!( + balanced_windows_sum::(windows, bits, |index| { + (index != failure).then_some(points[index]) + }) + .is_none() + ); + } + } + } + } + + #[test] + #[cfg(feature = "multicore")] + fn parallel_windows_match_weighted_sum_on_both_curves() { + parallel_windows_match_weighted_sum::(); + parallel_windows_match_weighted_sum::(); + } + fn batch_invert_nonzero_matches_individual() where F: Field + From, diff --git a/crates/pasta_curves/src/glv/orbit.rs b/crates/pasta_curves/src/glv/orbit.rs index 872a46a0..6af98acc 100644 --- a/crates/pasta_curves/src/glv/orbit.rs +++ b/crates/pasta_curves/src/glv/orbit.rs @@ -551,8 +551,8 @@ pub(super) fn windows_sum( /// The orbit-backend MSM over decomposed components. Callers guarantee the /// components respect the [`GLV_COMPONENT_BITS`] bound (as /// [`super::checked_signed_magnitudes`] enforces). Parallel runs schedule -/// the windows through the shared paired-window driver -/// ([`super::paired_windows_sum`]). +/// the windows through the shared balanced reduction tree +/// ([`super::balanced_windows_sum`]). #[cfg(feature = "orbits")] pub(super) fn multiexp( components: &[(SignedMagnitude, SignedMagnitude)], @@ -571,7 +571,7 @@ pub(super) fn multiexp( let _ = num_threads; #[cfg(feature = "multicore")] if num_threads > 1 { - return super::paired_windows_sum::(active_windows, window_bits, |window| { + return super::balanced_windows_sum::(active_windows, window_bits, |window| { windows_sum::(¶ms, &digits, &rotated, window..window + 1) }); } diff --git a/crates/pasta_curves/src/glv/zero.rs b/crates/pasta_curves/src/glv/zero.rs index f3292c3f..673c8676 100644 --- a/crates/pasta_curves/src/glv/zero.rs +++ b/crates/pasta_curves/src/glv/zero.rs @@ -141,7 +141,7 @@ const DEFAULT_TABLE_FOOTPRINT_BUDGET: usize = 13 << 20; #[derive(Clone, Copy)] enum MainWindowFold { - Paired, + Balanced, Horner, } @@ -323,7 +323,7 @@ impl PreparedZeroMsm { extra: &[(C::ScalarExt, C::AffineExt)], ) -> bool { bool::from( - self.multiexp_with_scalar_slices_using(scalars, &[], extra, MainWindowFold::Paired) + self.multiexp_with_scalar_slices_using(scalars, &[], extra, MainWindowFold::Balanced) .is_identity(), ) } @@ -554,13 +554,13 @@ impl PreparedZeroMsm { if num_threads > 1 { // Point-returning MSMs reduce the main windows independently // and combine them with one Horner fold. Zero checks retain the - // paired schedule tuned for an isolated MSM. The residual tail + // balanced reduction tree. The residual tail // and extras MSM run concurrently with the main windows. let main = || { maybe_rayon::join( || match main_window_fold { - MainWindowFold::Paired => { - super::paired_windows_sum::(active, window_bits, |window| { + MainWindowFold::Balanced => { + super::balanced_windows_sum::(active, window_bits, |window| { self.window_sum(recoded, window) }) } @@ -829,7 +829,7 @@ fn tail_multiexp( .map(|(row, &(first, second))| orbit::recode_row(params, first, second, row)) .max() .unwrap_or(0); - return super::paired_windows_sum::(active, params.width(), |window| { + return super::balanced_windows_sum::(active, params.width(), |window| { orbit::windows_sum::(params, &digits, rotated, window..window + 1) }); } diff --git a/docs/changelog/unreleased/372.md b/docs/changelog/unreleased/372.md new file mode 100644 index 00000000..6da0c7d6 --- /dev/null +++ b/docs/changelog/unreleased/372.md @@ -0,0 +1,13 @@ +## zakura-halo2-proofs + +### Changed + +- Reduced IPA proving work when using prepared `orbits` commitments + ([#372](https://github.com/zakura-core/common/pull/372)). + +## zakura-pasta-curves + +### Changed + +- Reduced duplicated point doublings in parallel orbit MSMs + ([#372](https://github.com/zakura-core/common/pull/372)).