Skip to content
Merged
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
2 changes: 1 addition & 1 deletion crates/halo2_proofs/src/plonk/keygen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn commit_fixed_lagrange<C: CurveAffine>(
params: &Params<C>,
polynomial: &Polynomial<C::Scalar, LagrangeCoeff>,
) -> C::Curve {
#[cfg(feature = "orbits")]
#[cfg(any(feature = "multicore", feature = "orbits"))]
if params.lagrange_table().is_some() {
return params.commit_lagrange(polynomial, Blind::default());
}
Expand Down
313 changes: 276 additions & 37 deletions crates/halo2_proofs/src/poly/commitment.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/halo2_proofs/src/poly/commitment/msm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use std::collections::BTreeMap;
/// 16 threads (Apple M4 Max) and +22–27% at a 32-thread pool
/// (32-hw-thread Skylake-X) — the crossover sits between 8 and 16 on both
/// architectures, on the assembly and portable field backends alike.
#[cfg(feature = "orbits")]
#[cfg(any(feature = "multicore", feature = "orbits"))]
pub(crate) const PREPARED_MSM_MAX_THREADS: usize = 8;

type ArbitraryTerm<C> = (
Expand Down
7 changes: 2 additions & 5 deletions crates/orchard/benches/orchard_k11_prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,8 @@ fn orchard_k11_prover(c: &mut Criterion) {
let vk = VerifyingKey::build(version);
let pk = ProvingKey::build(version);
// Keep the one-time table build outside the timed proving routine.
#[cfg(feature = "orbits")]
assert!(
pk.prepare_proving(),
"Pasta commitment tables must prepare with the orbits feature",
);
#[cfg(any(feature = "multicore", feature = "orbits"))]
assert!(pk.prepare_proving(), "Pasta commitment tables must prepare",);

let mut group = c.benchmark_group("orchard-k11");
group.sample_size(BENCHMARK_SAMPLES);
Expand Down
14 changes: 7 additions & 7 deletions crates/orchard/src/circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1172,25 +1172,25 @@ pub struct ProvingKey {
impl ProvingKey {
/// Builds and caches prepared fixed-base commitment tables over this
/// key's SRS (see
/// `halo2_proofs::poly::commitment::Params::prepare_commitments`).
/// [`halo2_proofs::poly::commitment::Params::prepare_commitments`]).
/// Long-lived provers (wallet backends, proving services) should call
/// this once after constructing the key: the prover's polynomial
/// commitments then evaluate through the preparations on pools of at
/// most eight effective threads, extended to ten on AArch64 macOS for
/// Orchard's `k = 11` SRS (measured end to end on Apple M4). Wider pools
/// retain their usual multiexp. One-shot provers need not prepare:
/// setup costs hundreds of milliseconds and tens of mebibytes,
/// amortized across proofs.
/// retain their usual multiexp. One-shot provers need not prepare: at
/// `k = 11`, the two tables account for about 24.8 MiB and took about
/// 34 ms to build on the benchmarked M4, amortized across proofs.
///
/// Call this once before entering concurrent Rayon proving work.
/// Concurrent callers outside that pool safely wait for and share the same
/// attempt; fanning a cold call out across the worker pool can occupy its
/// other workers and serialize the initializer's parallel work.
///
/// Returns whether the tables were actually built and cached; `false`
/// means arming was a no-op (Orchard was built without its opt-in
/// `orbits` feature, or its backend declined) and proving simply keeps
/// its unprepared path. Callers may ignore the result.
/// means arming was a no-op (Orchard was built with neither `multicore`
/// nor `orbits`, or its backend declined) and proving simply keeps its
/// unprepared path. Callers may ignore the result.
pub fn prepare_proving(&self) -> bool {
self.params.prepare_commitments()
}
Expand Down
7 changes: 4 additions & 3 deletions crates/pasta_curves/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ gpu = ["alloc", "ec-gpu"]
# `glv.rs` through the Rayon pool; implies `glv` (and therefore `alloc`).
multicore = ["glv", "dep:maybe-rayon", "maybe-rayon/threads"]
# The Eisenstein-orbit MSM backend (`glv::orbit`, planned against the
# Signed-Booth backend per input) and the prepared fixed-base zero-checks
# (`glv::zero`, exposed through `CurveExt::try_prepare_zero_check`).
# Without it the arbitrary-scalar MSM plans the Signed-Booth backend alone.
# Signed-Booth backend per input) and public prepared zero-check module
# (`glv::zero`). Without it arbitrary-scalar MSMs plan only the Signed-Booth
# backend; multicore callers can still use the private prepared evaluator
# through `CurveExt::try_prepare_zero_check`.
orbits = ["glv"]
repr-c = []
serde = ["hex", "serde_crate"]
Expand Down
25 changes: 13 additions & 12 deletions crates/pasta_curves/src/arithmetic/curves.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,18 @@ pub trait CurveExt:
/// backend return `None`, and implementations may also decline —
/// the Pasta backend returns `None` when its prepared table for this
/// many bases would exceed its internal table-footprint budget.
/// Preparation
/// can cost hundreds of milliseconds and tens of mebibytes for a few
/// thousand bases, so callers should invoke this once and reuse the
/// handle across checks.
/// Preparation can cost hundreds of milliseconds and tens of mebibytes
/// for a few thousand bases, so callers should invoke this once and
/// reuse the handle across checks.
///
/// # Security
///
/// The returned check runs in variable time with respect to scalars
/// and points. **All inputs must be public.**
#[cfg(feature = "orbits")]
#[cfg_attr(docsrs, doc(cfg(feature = "orbits")))]
/// The returned handle runs in variable time with respect to scalars and
/// points. Inputs to its zero-check methods must be public. Callers using
/// [`PreparedZeroCheck::multiexp_with_terms_vartime`] with secret scalars
/// must explicitly accept the timing side channel of a variable-time MSM.
#[cfg(any(feature = "multicore", feature = "orbits"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "multicore", feature = "orbits"))))]
fn try_prepare_zero_check(
bases: &[Self::AffineExt],
) -> Option<Box<dyn PreparedZeroCheck<Self>>> {
Expand All @@ -181,10 +182,10 @@ pub trait CurveExt:
/// for the fixed bases $P_i$ captured at preparation plus per-check
/// `extra` terms $(s_j, Q_j)$. Obtained from
/// [`CurveExt::try_prepare_zero_check`]; the Pasta curves implement it
/// with the prepared codebook backend (the `glv::zero` module), and the
/// check is exact — it accepts iff the sum is the identity.
#[cfg(feature = "orbits")]
#[cfg_attr(docsrs, doc(cfg(feature = "orbits")))]
/// with an internal prepared codebook backend, and the check is exact — it
/// accepts iff the sum is the identity.
#[cfg(any(feature = "multicore", feature = "orbits"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "multicore", feature = "orbits"))))]
pub trait PreparedZeroCheck<C: CurveExt>: core::fmt::Debug + Send + Sync {
/// The number of fixed bases this preparation covers; `scalars` below
/// must have exactly this length.
Expand Down
10 changes: 2 additions & 8 deletions crates/pasta_curves/src/curves.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,11 @@ macro_rules! impl_multiexp_vartime {
#[cfg(feature = "alloc")]
macro_rules! impl_prepare_zero_check {
(glv, $name:ident) => {
#[cfg(feature = "orbits")]
#[cfg(any(feature = "multicore", feature = "orbits"))]
fn try_prepare_zero_check(
bases: &[Self::AffineExt],
) -> Option<alloc::boxed::Box<dyn crate::arithmetic::PreparedZeroCheck<Self>>> {
// `prepare` declines (None) when no codebook mode fits its 13 MiB
// accounted-footprint budget — from roughly 2^13 Pasta bases — so
// callers fall back instead of allocating past it.
crate::glv::zero::PreparedZeroMsm::<$name>::prepare(bases).map(|prepared| {
alloc::boxed::Box::new(prepared)
as alloc::boxed::Box<dyn crate::arithmetic::PreparedZeroCheck<Self>>
})
crate::glv::prepare_zero_check::<$name>(bases)
}
};
(native, $name:ident) => {};
Expand Down
29 changes: 23 additions & 6 deletions crates/pasta_curves/src/glv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@
//! integrates buckets with a hexagonal spanning-tree reducer. Both fill their
//! buckets through the shared batched-affine tree reduction below
//! (`reduce_affine_buckets`, one fused inversion-and-completion pass per tree
//! level). The orbit backend, the planning step, and the prepared zero-checks
//! built on the same machinery (the `zero` submodule) are gated behind the
//! `orbits` feature; without it large MSMs window through the Signed-Booth
//! backend alone.
//! level). The orbit backend, its planning step, and the public `zero`
//! submodule are gated behind the `orbits` feature. Without it,
//! large MSMs use the Signed-Booth backend alone. Multicore builds still
//! compile the prepared evaluator privately for fixed-base callers through
//! [`CurveExt::try_prepare_zero_check`].
//!
//! This path is variable-time in the scalar (GLV decomposition plus digit
//! recoding); the `_glv` naming distinguishes it from the native `Mul`
Expand Down Expand Up @@ -89,11 +90,13 @@ use maybe_rayon::prelude::*;
use crate::arithmetic::{CurveExt, mac, sbb};
use crate::{pallas, vesta};

#[cfg(feature = "orbits")]
#[cfg(any(feature = "multicore", feature = "orbits"))]
mod orbit;
#[cfg(feature = "orbits")]
#[cfg_attr(docsrs, doc(cfg(feature = "orbits")))]
pub mod zero;
#[cfg(all(feature = "multicore", not(feature = "orbits")))]
mod zero;

mod private {
use crate::arithmetic::CurveExt;
Expand Down Expand Up @@ -178,6 +181,18 @@ mod private {
}
}

#[cfg(any(feature = "multicore", feature = "orbits"))]
pub(super) fn prepare_zero_check<C: GlvParams>(
bases: &[C::AffineExt],
) -> Option<alloc::boxed::Box<dyn crate::arithmetic::PreparedZeroCheck<C>>> {
// `prepare` declines when no codebook mode fits its 13 MiB
// accounted-footprint budget.
zero::PreparedZeroMsm::<C>::prepare(bases).map(|prepared| {
alloc::boxed::Box::new(prepared)
as alloc::boxed::Box<dyn crate::arithmetic::PreparedZeroCheck<C>>
})
}

/// Per-curve GLV constants: a short basis for the lattice
/// $\{(a, b) : a + b\lambda \equiv 0 \pmod n\}$ — where $n$ is the order of the
/// group (equivalently the scalar field modulus) and $\lambda$ = `Scalar::ZETA`
Expand Down Expand Up @@ -543,7 +558,9 @@ fn joint_digits(mut a: i128, mut b: i128) -> ([u8; MAX_JOINT_DIGITS], usize) {
const BATCH_AFFINE_MIN_POINTS: usize = 32;
// This range is tuned for the k = 11 parameter generation used by Orchard.
// Larger domains retain the point-major schedule above the measured range.
#[cfg(feature = "multicore")]
const TWIDDLE_MAJOR_MIN_CHUNK: usize = 16;
#[cfg(feature = "multicore")]
const TWIDDLE_MAJOR_MAX_CHUNK: usize = 2048;

/// Montgomery-batched inversion for a nonempty slice of provably nonzero
Expand Down Expand Up @@ -1765,7 +1782,7 @@ fn multiexp_serial<C: GlvParams>(
/// `window_sum` (an arithmetic guard) propagates out. Shared by the
/// Eisenstein-orbit backend and the prepared zero-check's main-window and
/// tail drivers.
#[cfg(all(feature = "multicore", feature = "orbits"))]
#[cfg(feature = "multicore")]
fn paired_windows_sum<C: GlvParams>(
windows: usize,
window_bits: usize,
Expand Down
20 changes: 15 additions & 5 deletions crates/pasta_curves/src/glv/orbit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,15 @@
use alloc::vec::Vec;

use ff::Field;
#[cfg(feature = "orbits")]
use group::CurveAffine as _;
#[cfg(feature = "multicore")]
#[cfg(all(feature = "multicore", feature = "orbits"))]
use maybe_rayon::prelude::*;

#[cfg(feature = "orbits")]
use super::MagnitudeProfile;
use super::{
AffinePoint, GLV_COMPONENT_BITS, GlvParams, MagnitudeProfile, SignedMagnitude, private,
reduce_affine_buckets,
AffinePoint, GLV_COMPONENT_BITS, GlvParams, SignedMagnitude, private, reduce_affine_buckets,
};

/// The window widths [`multiexp`] supports. Wider than 6 needs
Expand All @@ -109,13 +111,15 @@ pub(super) const MAX_WINDOW_BITS: usize = 6;
/// $(4^3 + 2)/6 = 11$ buckets only ever modeled ahead on MSMs of a few
/// hundred terms, where they measured 4–17% *behind* the Booth backend
/// (per-window overhead dominates 200-odd visits); planning starts at 4.
#[cfg(feature = "orbits")]
pub(super) const PLAN_MIN_WINDOW_BITS: usize = 4;
/// The smallest MSM [`estimated_costs`] will price for the planner. At 256
/// terms the backend's fixed per-window costs measured it 2–7% behind
/// Booth at low thread counts (sub-millisecond, noise-prone cells); from
/// 512 terms up it wins. This gates on the *term count*, not liveness —
/// sparse-but-large MSMs stay profitable (witness-shaped 2048-term inputs
/// measured +16..20% over Booth).
#[cfg(feature = "orbits")]
pub(super) const PLAN_MIN_TERMS: usize = 512;

/// Marks a wedge node whose reducer-tree parent is the origin.
Expand Down Expand Up @@ -311,6 +315,7 @@ impl OrbitParams {
}

/// The window width $c$ these parameters were built for.
#[cfg(feature = "multicore")]
pub(super) fn width(&self) -> usize {
self.window_bits
}
Expand All @@ -327,6 +332,7 @@ pub(super) struct RotatedBase<F> {
pub(super) y: F,
}

#[cfg(feature = "orbits")]
pub(super) fn rotate_base<C: GlvParams>(base: &C::AffineExt) -> RotatedBase<C::Base> {
let (x, y) = C::affine_xy(base);
let xz = x * <C::Base as ff::WithSmallOrderMulGroup<3>>::ZETA;
Expand All @@ -336,6 +342,7 @@ pub(super) fn rotate_base<C: GlvParams>(base: &C::AffineExt) -> RotatedBase<C::B
}
}

#[cfg(feature = "orbits")]
fn rotated_bases<C: GlvParams>(
bases: &[C::AffineExt],
num_threads: usize,
Expand Down Expand Up @@ -392,6 +399,7 @@ pub(super) fn recode_row(
/// zero so the window fills never test bases again. Small-magnitude
/// workloads recode to far fewer active windows than the bound, and the
/// drivers walk only those.
#[cfg(feature = "orbits")]
fn digit_matrix<C: GlvParams>(
params: &OrbitParams,
components: &[(SignedMagnitude, SignedMagnitude)],
Expand Down Expand Up @@ -545,6 +553,7 @@ pub(super) fn windows_sum<C: GlvParams>(
/// [`super::checked_signed_magnitudes`] enforces). Parallel runs schedule
/// the windows through the shared paired-window driver
/// ([`super::paired_windows_sum`]).
#[cfg(feature = "orbits")]
pub(super) fn multiexp<C: GlvParams>(
components: &[(SignedMagnitude, SignedMagnitude)],
bases: &[C::AffineExt],
Expand All @@ -570,7 +579,7 @@ pub(super) fn multiexp<C: GlvParams>(
}

/// Test convenience: the work component of [`estimated_costs`].
#[cfg(test)]
#[cfg(all(test, feature = "orbits"))]
pub(super) fn estimated_work(
profile: &MagnitudeProfile,
window_bits: usize,
Expand Down Expand Up @@ -616,6 +625,7 @@ pub(super) fn estimated_work(
/// (its $\lceil W/\text{workers}\rceil$ windows plus the top window's shift
/// doublings) — work stealing achieves the former at low worker counts;
/// the latter binds when workers exceed half the window count.
#[cfg(feature = "orbits")]
pub(super) fn estimated_costs(
profile: &MagnitudeProfile,
window_bits: usize,
Expand Down Expand Up @@ -676,7 +686,7 @@ pub(super) fn estimated_costs(
Some((balanced.max(quantized), traffic))
}

#[cfg(test)]
#[cfg(all(test, feature = "orbits"))]
mod tests {
use super::super::{GlvParams, decompose, digit_scalar, testutil};
use super::*;
Expand Down
Loading
Loading