Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
37 changes: 35 additions & 2 deletions crates/halo2_proofs/src/poly/commitment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,39 @@ impl<C: CurveAffine> Params<C> {
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;
Expand Down Expand Up @@ -1606,7 +1639,7 @@ impl<C: CurveAffine> Params<C> {
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
Expand Down Expand Up @@ -1641,7 +1674,7 @@ impl<C: CurveAffine> Params<C> {
/// 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
Expand Down
28 changes: 15 additions & 13 deletions crates/halo2_proofs/src/poly/commitment/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -392,25 +394,25 @@ fn parallel_generator_collapse<C: CurveAffine>(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};
use ff::Field;
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;

Expand Down Expand Up @@ -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<C>()
where
C: CurveAffine + core::fmt::Debug,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -853,19 +855,19 @@ mod tests {
round_multiexp_matches_split::<vesta::Affine>();
}

#[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::<pallas::Affine>();
}

#[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::<vesta::Affine>();
}

#[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();
Expand Down
105 changes: 72 additions & 33 deletions crates/pasta_curves/src/glv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1826,49 +1826,45 @@ fn multiexp_serial<C: GlvParams>(
}

/// $\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<C: GlvParams>(
fn balanced_windows_sum<C: GlvParams>(
windows: usize,
window_bits: usize,
window_sum: impl Fn(usize) -> Option<C> + Sync,
) -> Option<C> {
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<C: GlvParams>(
start: usize,
windows: usize,
window_bits: usize,
window_sum: &(impl Fn(usize) -> Option<C> + Sync),
) -> Option<C> {
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
Expand Down Expand Up @@ -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<C: GlvParams>() {
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::<C>(windows, bits, |index| Some(points[index])),
Some(expected),
"windows={windows}, bits={bits}",
);
for failure in 0..windows {
assert!(
balanced_windows_sum::<C>(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::<crate::pallas::Point>();
parallel_windows_match_weighted_sum::<crate::vesta::Point>();
}

fn batch_invert_nonzero_matches_individual<F>()
where
F: Field + From<u64>,
Expand Down
6 changes: 3 additions & 3 deletions crates/pasta_curves/src/glv/orbit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,8 +551,8 @@ pub(super) fn windows_sum<C: GlvParams>(
/// 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<C: GlvParams>(
components: &[(SignedMagnitude, SignedMagnitude)],
Expand All @@ -571,7 +571,7 @@ pub(super) fn multiexp<C: GlvParams>(
let _ = num_threads;
#[cfg(feature = "multicore")]
if num_threads > 1 {
return super::paired_windows_sum::<C>(active_windows, window_bits, |window| {
return super::balanced_windows_sum::<C>(active_windows, window_bits, |window| {
windows_sum::<C>(&params, &digits, &rotated, window..window + 1)
});
}
Expand Down
12 changes: 6 additions & 6 deletions crates/pasta_curves/src/glv/zero.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ const DEFAULT_TABLE_FOOTPRINT_BUDGET: usize = 13 << 20;

#[derive(Clone, Copy)]
enum MainWindowFold {
Paired,
Balanced,
Horner,
}

Expand Down Expand Up @@ -323,7 +323,7 @@ impl<C: GlvParams> PreparedZeroMsm<C> {
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(),
)
}
Expand Down Expand Up @@ -554,13 +554,13 @@ impl<C: GlvParams> PreparedZeroMsm<C> {
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::<C>(active, window_bits, |window| {
MainWindowFold::Balanced => {
super::balanced_windows_sum::<C>(active, window_bits, |window| {
self.window_sum(recoded, window)
})
}
Expand Down Expand Up @@ -829,7 +829,7 @@ fn tail_multiexp<C: GlvParams>(
.map(|(row, &(first, second))| orbit::recode_row(params, first, second, row))
.max()
.unwrap_or(0);
return super::paired_windows_sum::<C>(active, params.width(), |window| {
return super::balanced_windows_sum::<C>(active, params.width(), |window| {
orbit::windows_sum::<C>(params, &digits, rotated, window..window + 1)
});
}
Expand Down
13 changes: 13 additions & 0 deletions docs/changelog/unreleased/372.md
Original file line number Diff line number Diff line change
@@ -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)).
Loading