diff --git a/halo2_proofs/Cargo.toml b/halo2_proofs/Cargo.toml index dc845181..7029b835 100644 --- a/halo2_proofs/Cargo.toml +++ b/halo2_proofs/Cargo.toml @@ -54,6 +54,7 @@ blake2b_simd = "1" maybe-rayon = {version = "0.1.0", default-features = false} # Developer tooling dependencies +criterion = { version = "0.3", optional = true } plotters = { version = "0.3.0", default-features = false, optional = true } tabbycat = { version = "0.1", features = ["attributes"], optional = true } @@ -73,6 +74,7 @@ getrandom = { version = "0.2", features = ["js"] } [features] default = ["batch", "multicore"] multicore = ["maybe-rayon/threads"] +dev-cost = ["criterion"] dev-graph = ["plotters", "tabbycat"] test-dev-graph = [ "dev-graph", diff --git a/halo2_proofs/src/dev/cost.rs b/halo2_proofs/src/dev/cost.rs index 0c3c7efa..1a13615b 100644 --- a/halo2_proofs/src/dev/cost.rs +++ b/halo2_proofs/src/dev/cost.rs @@ -3,13 +3,16 @@ use std::{ cmp, collections::{HashMap, HashSet}, - iter, + iter::{self, Sum}, marker::PhantomData, ops::{Add, Mul}, + time::{Duration, Instant}, }; use ff::{Field, PrimeField}; -use group::prime::PrimeGroup; +use group::{prime::PrimeGroup, Group}; +use pasta_curves::arithmetic::{CurveAffine, CurveExt}; +use rand_core::OsRng; use crate::{ circuit::{layouter::RegionColumn, Value}, @@ -20,6 +23,8 @@ use crate::{ poly::Rotation, }; +mod time; + /// Measures a circuit to determine its costs, and explain what contributes to them. #[allow(dead_code)] #[derive(Debug)] @@ -28,8 +33,12 @@ pub struct CircuitCost> { k: u32, /// Maximum degree of the circuit. max_deg: usize, + /// Number of instance columns. + instance_columns: usize, /// Number of advice columns. advice_columns: usize, + /// Costs of the gate expressions. + gate_expressions: Vec, /// Number of direct queries for each column type. instance_queries: usize, advice_queries: usize, @@ -38,8 +47,8 @@ pub struct CircuitCost> { lookups: usize, /// Number of columns in the global permutation. permutation_cols: usize, - /// Number of distinct sets of points in the multiopening argument. - point_sets: usize, + /// Size of each distinct sets of points in the multiopening argument. + point_sets: Vec, /// Maximum rows used over all columns max_rows: usize, /// Maximum rows used over all advice columns @@ -274,6 +283,27 @@ impl> CircuitCost= cs.minimum_rows()); + // Measure the gate expressions. + let gate_expressions = cs + .gates + .iter() + .flat_map(move |gate| { + gate.polynomials().iter().map(move |poly| { + poly.evaluate( + &|_| (0, 0, 0).into(), + &|_| panic!("virtual selectors are removed during optimization"), + &|_, _, _| (0, 0, 0).into(), + &|_, _, _| (0, 0, 0).into(), + &|_, _, _| (0, 0, 0).into(), + &|a| a, + &|a, b| a + b + (1, 0, 0).into(), + &|a, b| a + b + (0, 1, 0).into(), + &|a, _| a + (0, 0, 1).into(), + ) + }) + }) + .collect(); + // Figure out how many point sets we have due to queried cells. let mut column_queries: HashMap, HashSet> = HashMap::new(); for (c, r) in iter::empty() @@ -318,13 +348,15 @@ impl> CircuitCost> CircuitCost> CircuitCost for ProofContribution { } /// The marginal size of a Halo 2 proof, broken down into its contributing factors. -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub struct MarginalProofSize { instance: ProofContribution, advice: ProofContribution, @@ -481,7 +513,7 @@ impl From> for usize { } /// The size of a Halo 2 proof, broken down into its contributing factors. -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub struct ProofSize { instance: ProofContribution, advice: ProofContribution, diff --git a/halo2_proofs/src/dev/cost/time.rs b/halo2_proofs/src/dev/cost/time.rs new file mode 100644 index 00000000..8b36bff3 --- /dev/null +++ b/halo2_proofs/src/dev/cost/time.rs @@ -0,0 +1,387 @@ +use std::{ + iter::{self, Sum}, + marker::PhantomData, + ops::{Add, Mul}, + time::{Duration, Instant}, +}; + +use group::{ff::Field, prime::PrimeGroup, Group}; +use pasta_curves::arithmetic::{CurveAffine, CurveExt}; + +use super::{CircuitCost, MarginalProofSize, ProofContribution, ProofSize}; +use crate::{ + plonk::Circuit, + transcript::{Blake2bRead, Transcript, TranscriptRead}, +}; + +impl::Scalar>> CircuitCost { + /// Returns the marginal verifying cost per instance of this circuit. + pub fn marginal_verifying(&self) -> MarginalVerifyingCost { + let chunks = self.permutation_chunks(); + + MarginalVerifyingCost { + // Transcript: + // - the marginal proof size + proof: self.marginal_proof_size(), + + // Cells: + // - 1 polynomial commitment per instance column per instance + // - 1 commitment per instance column per instance + instance_columns: self.instance_columns, + + // Gates: + gates: TimeContribution::new(0, self.gate_expressions.iter().copied()), + + // Lookup arguments: + // - 7 additions per lookup argument per instance + // - 6 multiplications per lookup argument per instance + lookups: TimeContribution::new(0, [(1, 1), (1, 2), (1, 1), (4, 2)].into_iter()) + * self.lookups, + + // Global permutation argument: + // - 5 * chunks + 3 additions and multiplications per instance + equality: TimeContribution::new( + 0, + [(1, 1)] + .into_iter() + .chain((0..chunks - 1).map(|_| (1, 1))) + .chain((0..chunks).map(|_| (4 * chunks + 3, 4 * chunks + 3))), + ), + + _marker: PhantomData::default(), + } + } + + /// Returns the verifying cost for the given number of instances of this circuit. + pub fn verifying(&self, instances: usize) -> VerifyingCost { + let marginal = self.marginal_verifying(); + + VerifyingCost { + // Transcript: + // - the proof + // - marginal cost per instance + proof: self.proof_size(instances), + instance_columns: marginal.instance_columns * instances, + + // - Verifying key + // - 5 challenges + + // Gates: + // - marginal cost per instance + gates: marginal.gates * instances, + + // Lookup arguments: + // - marginal cost per instance + lookups: marginal.lookups * instances, + + // Global permutation argument: + // - marginal cost per instance + // - TODO: global cost + equality: marginal.equality * instances, + + // Vanishing argument: + // - expressions + 1 commitments + // - 1 random_poly eval + vanishing: TimeContribution::new(0, [(1, 1)].into_iter()), + + // Multiopening argument: + // - TODO: point set evals + // - TODO: Lagrange interpolation per point set + // - TODO: inversions + // - 2 additions and mults per point in multiopen argument + // - 2 additions per set of points in multiopen argument + // - 1 multiplication per set of points in multiopen argument + multiopen: self + .point_sets + .iter() + .map(|points| TimeContribution::new(0, (2 * points + 2, 2 * points + 1).into())) + .sum(), + + // Polycommit: + // - s_poly commitment + // - inner product argument (2 * k round commitments) + // - a + // - xi + polycomm: ProofContribution::new(1 + 2 * self.k, 2), + + _marker: PhantomData::default(), + } + } +} + +/// The marginal time cost of verifying a specific Halo 2 proof, broken down into its +/// contributing factors. +#[derive(Clone, Debug)] +pub struct MarginalVerifyingCost { + proof: MarginalProofSize, + instance_columns: usize, + gates: TimeContribution, + lookups: TimeContribution, + equality: TimeContribution, + _marker: PhantomData, +} + +impl MarginalVerifyingCost { + fn transcript_inputs(&self) -> ProofContribution { + self.proof.instance + self.proof.advice + self.proof.lookups + self.proof.equality + } + + fn expressions(&self) -> ExpressionCost { + iter::empty() + .chain(self.gates.expressions.iter()) + .chain(self.lookups.expressions.iter()) + .chain(self.equality.expressions.iter()) + .sum() + } + + /// Estimates the concrete time cost for verifying this proof. + #[cfg(feature = "dev-cost")] + pub fn estimate(&self) -> TimeCost { + TimeCost::estimate::( + self.instance_columns, + self.transcript_inputs(), + self.expressions(), + ) + } + + /// Evaluates the concrete time cost for verifying this proof. + pub fn evaluate(&self, single_field_add: Duration, single_field_mul: Duration) -> TimeCost { + TimeCost::evaluate::( + self.instance_columns, + self.transcript_inputs(), + self.expressions(), + single_field_add, + single_field_mul, + ) + } +} + +/// The time cost of verifying a specific Halo 2 proof, broken down into its contributing +/// factors. +#[derive(Clone, Debug)] +pub struct VerifyingCost { + proof: ProofSize, + instance_columns: usize, + gates: TimeContribution, + lookups: TimeContribution, + equality: TimeContribution, + vanishing: TimeContribution, + multiopen: TimeContribution, + polycomm: TimeContribution, + _marker: PhantomData, +} + +impl VerifyingCost { + fn transcript_inputs(&self) -> ProofContribution { + // TODO + self.proof.instance + self.proof.advice + self.proof.lookups + self.proof.equality + } + + fn expressions(&self) -> ExpressionCost { + iter::empty() + .chain(self.gates.expressions.iter()) + .chain(self.lookups.expressions.iter()) + .chain(self.equality.expressions.iter()) + .chain(self.vanishing.expressions.iter()) + .chain(self.multiopen.expressions.iter()) + .chain(self.polycomm.expressions.iter()) + .sum() + } + + /// Estimates the concrete time cost for verifying this proof. + #[cfg(feature = "dev-cost")] + pub fn estimate(&self) -> TimeCost { + TimeCost::estimate::( + self.instance_columns, + self.transcript_inputs(), + self.expressions(), + ) + } + + /// Evaluates the concrete time cost for verifying this proof. + pub fn evaluate(&self, single_field_add: Duration, single_field_mul: Duration) -> TimeCost { + TimeCost::evaluate::( + self.instance_columns, + self.transcript_inputs(), + self.expressions(), + single_field_add, + single_field_mul, + ) + } +} + +/// The estimated time cost of proving or verifying a specific Halo 2 circuit. +#[derive(Clone, Copy, Debug)] +pub struct TimeCost { + transcript: Duration, + expressions: Duration, +} + +impl TimeCost { + #[cfg(feature = "dev-cost")] + fn estimate( + instance_columns: usize, + transcript_inputs: ProofContribution, + expressions: ExpressionCost, + ) -> Self { + use rand_core::OsRng; + + let pairs = [0; 100].map(|_| (C::Scalar::random(OsRng), C::Scalar::random(OsRng))); + + let runner = |f: fn(C::Scalar, C::Scalar) -> C::Scalar| { + let start = Instant::now(); + for _ in 0..100 { + for (a, b) in pairs.into_iter() { + let _ = criterion::black_box(f(a, b)); + } + } + Instant::now().duration_since(start) / (100 * pairs.len() as u32) + }; + + let single_field_add = runner(|a, b| a + b); + let single_field_mul = runner(|a, b| a * b); + + Self::evaluate::( + instance_columns, + transcript_inputs, + expressions, + single_field_add, + single_field_mul, + ) + } + + fn evaluate( + instance_columns: usize, + transcript_inputs: ProofContribution, + expressions: ExpressionCost, + single_field_add: Duration, + single_field_mul: Duration, + ) -> Self { + let dummy_point = C::generator(); + + // Transcript cost + let transcript = { + let mut transcript = Blake2bRead::init(std::io::repeat(1)); + let start = Instant::now(); + for _ in 0..instance_columns { + let _ = transcript.common_point(dummy_point).unwrap(); + } + for _ in 0..transcript_inputs.commitments { + let _ = transcript.read_point().unwrap(); + } + for _ in 0..transcript_inputs.evaluations { + let _ = transcript.read_scalar().unwrap(); + } + Instant::now().duration_since(start) + }; + + // Expressions cost + let expressions = single_field_add * expressions.add as u32 + + single_field_mul * (expressions.mul + expressions.scale) as u32; + + Self { + transcript, + expressions, + } + } + + /// Returns the total estimated time cost. + pub fn total(&self) -> Duration { + self.transcript + self.expressions + } +} + +#[derive(Clone, Copy, Debug)] +pub(super) struct ExpressionCost { + add: usize, + mul: usize, + scale: usize, +} + +/// Use when multiplications and scalings have the same cost. +impl From<(usize, usize)> for ExpressionCost { + fn from((add, mul): (usize, usize)) -> Self { + Self { add, mul, scale: 0 } + } +} + +impl From<(usize, usize, usize)> for ExpressionCost { + fn from((add, mul, scale): (usize, usize, usize)) -> Self { + Self { add, mul, scale } + } +} + +impl Add for ExpressionCost { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self { + add: self.add + rhs.add, + mul: self.mul + rhs.mul, + scale: self.scale + rhs.scale, + } + } +} + +impl<'a> Sum<&'a ExpressionCost> for ExpressionCost { + fn sum>(iter: I) -> Self { + iter.fold((0, 0, 0).into(), |acc, expr| acc + *expr) + } +} + +#[derive(Clone, Debug)] +struct TimeContribution { + polynomial_commitments: usize, + expressions: Vec, +} + +impl TimeContribution { + fn new(polynomial_commitments: usize, expressions: impl Iterator) -> Self + where + ExpressionCost: From, + { + Self { + polynomial_commitments, + expressions: expressions.map(ExpressionCost::from).collect(), + } + } +} + +impl Add for TimeContribution { + type Output = Self; + + fn add(self, mut rhs: Self) -> Self::Output { + let mut expressions = self.expressions; + expressions.append(&mut rhs.expressions); + Self { + polynomial_commitments: self.polynomial_commitments + rhs.polynomial_commitments, + expressions, + } + } +} + +impl Mul for TimeContribution { + type Output = Self; + + fn mul(self, instances: usize) -> Self::Output { + Self { + polynomial_commitments: self.polynomial_commitments * instances, + expressions: iter::repeat(self.expressions.into_iter()) + .take(instances) + .flatten() + .collect(), + } + } +} + +impl Sum for TimeContribution { + fn sum>(iter: I) -> Self { + iter.fold( + TimeContribution { + polynomial_commitments: 0, + expressions: vec![], + }, + |acc, expr| acc + expr, + ) + } +}