From b310505b4e2105dbc6c29d4d203e979b8ae88f1a Mon Sep 17 00:00:00 2001 From: Henry Blanchette Date: Mon, 9 Oct 2023 23:47:21 +0800 Subject: [PATCH] Benchmarking tool using cost estimator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: IƱigo Querejeta Azurmendi --- .gitignore | 1 + halo2_proofs/Cargo.toml | 2 + halo2_proofs/examples/cost-model.rs | 267 +----------------- halo2_proofs/src/dev.rs | 2 + halo2_proofs/src/dev/cost_model.rs | 413 ++++++++++++++++++++++++++++ 5 files changed, 433 insertions(+), 252 deletions(-) create mode 100644 halo2_proofs/src/dev/cost_model.rs diff --git a/.gitignore b/.gitignore index f2af733bf1..dc66c847c6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Cargo.lock .vscode **/*.html .DS_Store +halo2_gadgets/proptest-regressions/utilities/ diff --git a/halo2_proofs/Cargo.toml b/halo2_proofs/Cargo.toml index cf62f69ce0..e41f144f37 100644 --- a/halo2_proofs/Cargo.toml +++ b/halo2_proofs/Cargo.toml @@ -58,6 +58,8 @@ blake2b_simd = "1" # MSRV 1.66.0 sha3 = "0.9.1" rand_chacha = "0.3" maybe-rayon = { version = "0.1.0", default-features = false } +rand = "0.8" +csv = "1.3.0" # Developer tooling dependencies plotters = { version = "0.3.0", default-features = false, optional = true } diff --git a/halo2_proofs/examples/cost-model.rs b/halo2_proofs/examples/cost-model.rs index 100f047fac..36c54fa7c8 100644 --- a/halo2_proofs/examples/cost-model.rs +++ b/halo2_proofs/examples/cost-model.rs @@ -1,53 +1,9 @@ -use std::{ - cmp, fmt, iter, - num::ParseIntError, - str::FromStr, - time::{Duration, Instant}, -}; - -use ff::Field; -use group::{Curve, Group}; use gumdrop::Options; -use halo2_proofs::arithmetic::best_multiexp; -use halo2curves::pasta::pallas; - -struct Estimator { - /// Scalars for estimating multiexp performance. - multiexp_scalars: Vec, - /// Bases for estimating multiexp performance. - multiexp_bases: Vec, -} - -impl fmt::Debug for Estimator { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Estimator") - } -} - -impl Estimator { - fn random(k: usize) -> Self { - let max_size = 1 << (k + 1); - let mut rng = rand_core::OsRng; - - Estimator { - multiexp_scalars: (0..max_size) - .map(|_| pallas::Scalar::random(&mut rng)) - .collect(), - multiexp_bases: (0..max_size) - .map(|_| pallas::Point::random(&mut rng).to_affine()) - .collect(), - } - } - - fn multiexp(&self, size: usize) -> Duration { - let start = Instant::now(); - best_multiexp(&self.multiexp_scalars[..size], &self.multiexp_bases[..size]); - Instant::now().duration_since(start) - } -} +use halo2_proofs::dev::cost_model::*; +/// CLI Options to build a circuit specifiction to measure the cost model of. #[derive(Debug, Options)] -struct CostOptions { +struct CliCostOptions { #[options(help = "Print this message.")] help: bool, @@ -85,216 +41,23 @@ struct CostOptions { k: usize, } -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -struct Poly { - rotations: Vec, -} - -impl FromStr for Poly { - type Err = ParseIntError; - - fn from_str(s: &str) -> Result { - let mut rotations: Vec = - s.split(',').map(|r| r.parse()).collect::>()?; - rotations.sort_unstable(); - Ok(Poly { rotations }) - } -} - -#[derive(Debug)] -struct Lookup { - _columns: usize, - input_deg: usize, - table_deg: usize, -} - -impl FromStr for Lookup { - type Err = ParseIntError; - - fn from_str(s: &str) -> Result { - let mut parts = s.split(','); - let _columns = parts.next().unwrap().parse()?; - let input_deg = parts.next().unwrap().parse()?; - let table_deg = parts.next().unwrap().parse()?; - Ok(Lookup { - _columns, - input_deg, - table_deg, - }) - } -} - -impl Lookup { - fn required_degree(&self) -> usize { - 2 + cmp::max(1, self.input_deg) + cmp::max(1, self.table_deg) - } - - fn queries(&self) -> impl Iterator { - // - product commitments at x and x_inv - // - input commitments at x and x_inv - // - table commitments at x - let product = "0,-1".parse().unwrap(); - let input = "0,-1".parse().unwrap(); - let table = "0".parse().unwrap(); - - iter::empty() - .chain(Some(product)) - .chain(Some(input)) - .chain(Some(table)) - } -} - -#[derive(Debug)] -struct Permutation { - columns: usize, -} - -impl FromStr for Permutation { - type Err = ParseIntError; - - fn from_str(s: &str) -> Result { - Ok(Permutation { - columns: s.parse()?, - }) - } -} - -impl Permutation { - fn required_degree(&self) -> usize { - cmp::max(self.columns + 1, 2) - } - - fn queries(&self) -> impl Iterator { - // - product commitments at x and x_inv - // - polynomial commitments at x - let product = "0,-1".parse().unwrap(); - let poly = "0".parse().unwrap(); - - iter::empty() - .chain(Some(product)) - .chain(iter::repeat(poly).take(self.columns)) - } -} - -#[derive(Debug)] -struct Circuit { - /// Power-of-2 bound on the number of rows in the circuit. - k: usize, - /// Maximum degree of the circuit. - max_deg: usize, - /// Number of advice columns. - advice_columns: usize, - /// Number of lookup arguments. - lookups: usize, - /// Equality constraint permutation arguments. - permutations: Vec, - /// Number of distinct column queries across all gates. - column_queries: usize, - /// Number of distinct sets of points in the multiopening argument. - point_sets: usize, - - estimator: Estimator, -} - -impl From for Circuit { - fn from(opts: CostOptions) -> Self { - let max_deg = [1, opts.gate_degree] - .iter() - .cloned() - .chain(opts.lookup.iter().map(|l| l.required_degree())) - .chain(opts.permutation.iter().map(|p| p.required_degree())) - .max() - .unwrap(); - - let mut queries: Vec<_> = iter::empty() - .chain(opts.advice.iter()) - .chain(opts.instance.iter()) - .chain(opts.fixed.iter()) - .cloned() - .chain(opts.lookup.iter().flat_map(|l| l.queries())) - .chain(opts.permutation.iter().flat_map(|p| p.queries())) - .chain(iter::repeat("0".parse().unwrap()).take(max_deg - 1)) - .collect(); - - let column_queries = queries.len(); - queries.sort_unstable(); - queries.dedup(); - let point_sets = queries.len(); - - Circuit { - k: opts.k, - max_deg, - advice_columns: opts.advice.len(), - lookups: opts.lookup.len(), - permutations: opts.permutation, - column_queries, - point_sets, - estimator: Estimator::random(opts.k), +impl CliCostOptions { + fn to_cost_options(&self) -> CostOptions { + CostOptions { + advice: self.advice.clone(), + instance: self.instance.clone(), + fixed: self.fixed.clone(), + gate_degree: self.gate_degree.clone(), + lookup: self.lookup.clone(), + permutation: self.permutation.clone(), + k: self.k, } } } -impl Circuit { - fn proof_size(&self) -> usize { - let size = |points: usize, scalars: usize| points * 32 + scalars * 32; - - // PLONK: - // - 32 bytes (commitment) per advice column - // - 3 * 32 bytes (commitments) + 5 * 32 bytes (evals) per lookup argument - // - 32 bytes (commitment) + 2 * 32 bytes (evals) per permutation argument - // - 32 bytes (eval) per column per permutation argument - let plonk = size(1, 0) * self.advice_columns - + size(3, 5) * self.lookups - + self - .permutations - .iter() - .map(|p| size(1, 2 + p.columns)) - .sum::(); - - // Vanishing argument: - // - (max_deg - 1) * 32 bytes (commitments) + (max_deg - 1) * 32 bytes (h_evals) - // for quotient polynomial - // - 32 bytes (eval) per column query - let vanishing = size(self.max_deg - 1, self.max_deg - 1) + size(0, self.column_queries); - - // Multiopening argument: - // - f_commitment (32 bytes) - // - 32 bytes (evals) per set of points in multiopen argument - let multiopen = size(1, self.point_sets); - - // Polycommit: - // - s_poly commitment (32 bytes) - // - inner product argument (k rounds * 2 * 32 bytes) - // - a (32 bytes) - // - xi (32 bytes) - let polycomm = size(1 + 2 * self.k, 2); - - plonk + vanishing + multiopen + polycomm - } - - fn verification_time(&self) -> Duration { - // TODO: Estimate cost of BLAKE2b. - - // TODO: This isn't accurate; most of these will have zero scalars. - let g_scalars = 1 << self.k; - - // - f_commitment - // - q_commitments - let multiopen = 1 + self.column_queries; - - // - \iota - // - Rounds - // - H - // - U - let polycomm = 1 + (2 * self.k) + 1 + 1; - - self.estimator.multiexp(g_scalars + multiopen + polycomm) - } -} - fn main() { - let opts = CostOptions::parse_args_default_or_exit(); - let c = Circuit::from(opts); + let opts = CliCostOptions::parse_args_default_or_exit(); + let c = ModelCircuit::from(opts.to_cost_options()); println!("{:#?}", c); println!("Proof size: {} bytes", c.proof_size()); println!( diff --git a/halo2_proofs/src/dev.rs b/halo2_proofs/src/dev.rs index 38c805b085..719147d7ed 100644 --- a/halo2_proofs/src/dev.rs +++ b/halo2_proofs/src/dev.rs @@ -36,6 +36,8 @@ pub use failure::{FailureLocation, VerifyFailure}; pub mod cost; pub use cost::CircuitCost; +pub mod cost_model; + mod gates; pub use gates::CircuitGates; diff --git a/halo2_proofs/src/dev/cost_model.rs b/halo2_proofs/src/dev/cost_model.rs new file mode 100644 index 0000000000..e2338ac88b --- /dev/null +++ b/halo2_proofs/src/dev/cost_model.rs @@ -0,0 +1,413 @@ +//! The cost estimator takes high-level parameters for a circuit design, and estimates the verification cost, as well as resulting proof size. + +use std::{ + cmp, fmt, iter, + num::ParseIntError, + str::FromStr, + time::{Duration, Instant}, +}; + +use csv::Writer; + +use crate::plonk; +use crate::{arithmetic::best_multiexp, plonk::Circuit}; +use ff::{Field, FromUniformBytes}; +use group::{Curve, Group}; +use halo2curves::pasta::pallas; + +use super::MockProver; + +/// Structure storing scalars and bases used in multiexp estimation +pub struct Estimator { + /// Scalars for estimating multiexp performance. + multiexp_scalars: Vec, + /// Bases for estimating multiexp performance. + multiexp_bases: Vec, +} + +impl fmt::Debug for Estimator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Estimator") + } +} + +impl Estimator { + fn random(k: usize) -> Self { + let max_size = 1 << (k + 1); + let mut rng = rand_core::OsRng; + + Estimator { + multiexp_scalars: (0..max_size) + .map(|_| pallas::Scalar::random(&mut rng)) + .collect(), + multiexp_bases: (0..max_size) + .map(|_| pallas::Point::random(&mut rng).to_affine()) + .collect(), + } + } + + fn multiexp(&self, size: usize) -> Duration { + let start = Instant::now(); + best_multiexp(&self.multiexp_scalars[..size], &self.multiexp_bases[..size]); + Instant::now().duration_since(start) + } +} + +/// Options to build a circuit specifiction to measure the cost model of. +#[derive(Debug)] +pub struct CostOptions { + /// An advice column with the given rotations. May be repeated. + pub advice: Vec, + + /// An instance column with the given rotations. May be repeated. + pub instance: Vec, + + /// A fixed column with the given rotations. May be repeated. + pub fixed: Vec, + + /// Maximum degree of the custom gates. + pub gate_degree: usize, + + /// A lookup over N columns with max input degree I and max table degree T. May be repeated. + pub lookup: Vec, + + /// A permutation over N columns. May be repeated. + pub permutation: Vec, + + /// 2^K bound on the number of rows. + pub k: usize, +} + +/// Structure holding polynomial related data for benchmarks +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Poly { + /// Rotations for the given polynomial + pub rotations: Vec, +} + +impl FromStr for Poly { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + let mut rotations: Vec = + s.split(',').map(|r| r.parse()).collect::>()?; + rotations.sort_unstable(); + Ok(Poly { rotations }) + } +} + +/// Structure holding the Lookup related data for circuit benchmarks. +#[derive(Debug, Clone)] +pub struct Lookup { + _columns: usize, + input_deg: usize, + table_deg: usize, +} + +impl FromStr for Lookup { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + let mut parts = s.split(','); + let _columns = parts.next().unwrap().parse()?; + let input_deg = parts.next().unwrap().parse()?; + let table_deg = parts.next().unwrap().parse()?; + Ok(Lookup { + _columns, + input_deg, + table_deg, + }) + } +} + +impl Lookup { + fn required_degree(&self) -> usize { + 2 + cmp::max(1, self.input_deg) + cmp::max(1, self.table_deg) + } + + fn queries(&self) -> impl Iterator { + // - product commitments at x and x_inv + // - input commitments at x and x_inv + // - table commitments at x + let product = "0,-1".parse().unwrap(); + let input = "0,-1".parse().unwrap(); + let table = "0".parse().unwrap(); + + iter::empty() + .chain(Some(product)) + .chain(Some(input)) + .chain(Some(table)) + } +} + +/// Number of permutation enabled columns +#[derive(Debug, Clone)] +pub struct Permutation { + columns: usize, +} + +impl FromStr for Permutation { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + Ok(Permutation { + columns: s.parse()?, + }) + } +} + +impl Permutation { + fn required_degree(&self) -> usize { + cmp::max(self.columns + 1, 2) + } + + fn queries(&self) -> impl Iterator { + // - product commitments at x and x_inv + // - polynomial commitments at x + let product = "0,-1".parse().unwrap(); + let poly = "0".parse().unwrap(); + + iter::empty() + .chain(Some(product)) + .chain(iter::repeat(poly).take(self.columns)) + } +} + +/// High-level specifications of an abstract circuit. +#[derive(Debug)] +pub struct ModelCircuit { + /// Power-of-2 bound on the number of rows in the circuit. + pub k: usize, + /// Maximum degree of the circuit. + pub max_deg: usize, + /// Number of advice columns. + pub advice_columns: usize, + /// Number of lookup arguments. + pub lookups: usize, + /// Equality constraint permutation arguments. + pub permutations: Vec, + /// Number of distinct column queries across all gates. + pub column_queries: usize, + /// Number of distinct sets of points in the multiopening argument. + pub point_sets: usize, + + /// Estimator of cost for the multiexp + pub estimator: Estimator, +} + +impl From for ModelCircuit { + fn from(opts: CostOptions) -> Self { + let max_deg = [1, opts.gate_degree] + .iter() + .cloned() + .chain(opts.lookup.iter().map(|l| l.required_degree())) + .chain(opts.permutation.iter().map(|p| p.required_degree())) + .max() + .unwrap(); + + let mut queries: Vec<_> = iter::empty() + .chain(opts.advice.iter()) + .chain(opts.instance.iter()) + .chain(opts.fixed.iter()) + .cloned() + .chain(opts.lookup.iter().flat_map(|l| l.queries())) + .chain(opts.permutation.iter().flat_map(|p| p.queries())) + .chain(iter::repeat("0".parse().unwrap()).take(max_deg - 1)) + .collect(); + + let column_queries = queries.len(); + queries.sort_unstable(); + queries.dedup(); + let point_sets = queries.len(); + + ModelCircuit { + k: opts.k, + max_deg, + advice_columns: opts.advice.len(), + lookups: opts.lookup.len(), + permutations: opts.permutation, + column_queries, + point_sets, + estimator: Estimator::random(opts.k), + } + } +} + +impl ModelCircuit { + /// Size of the proof in bytes + pub fn proof_size(&self) -> usize { + let size = |points: usize, scalars: usize| points * 32 + scalars * 32; + + // PLONK: + // - 32 bytes (commitment) per advice column + // - 3 * 32 bytes (commitments) + 5 * 32 bytes (evals) per lookup argument + // - 32 bytes (commitment) + 2 * 32 bytes (evals) per permutation argument + // - 32 bytes (eval) per column per permutation argument + let plonk = size(1, 0) * self.advice_columns + + size(3, 5) * self.lookups + + self + .permutations + .iter() + .map(|p| size(1, 2 + p.columns)) + .sum::(); + + // Vanishing argument: + // - (max_deg - 1) * 32 bytes (commitments) + (max_deg - 1) * 32 bytes (h_evals) + // for quotient polynomial + // - 32 bytes (eval) per column query + let vanishing = size(self.max_deg - 1, self.max_deg - 1) + size(0, self.column_queries); + + // Multiopening argument: + // - f_commitment (32 bytes) + // - 32 bytes (evals) per set of points in multiopen argument + let multiopen = size(1, self.point_sets); + + // Polycommit: + // - s_poly commitment (32 bytes) + // - inner product argument (k rounds * 2 * 32 bytes) + // - a (32 bytes) + // - xi (32 bytes) + let polycomm = size(1 + 2 * self.k, 2); + + plonk + vanishing + multiopen + polycomm + } + + /// Measures estimated verification time (based on doing a real-time measurement on multi-exponentiation using `Estimator`). + pub fn verification_time(&self) -> Duration { + // TODO: Estimate cost of BLAKE2b. + + // TODO: This isn't accurate; most of these will have zero scalars. + let g_scalars = 1 << self.k; + + // - f_commitment + // - q_commitments + let multiopen = 1 + self.column_queries; + + // - \iota + // - Rounds + // - H + // - U + let polycomm = 1 + (2 * self.k) + 1 + 1; + + self.estimator.multiexp(g_scalars + multiopen + polycomm) + } + + /// Generate a report. + pub fn report(&self) -> String { + let mut str = String::new(); + str.push_str(&format!("{:#?}", self)); + str.push_str(&format!("Proof size: {} bytes", self.proof_size())); + str.push_str(&format!( + "Verification: at least {}ms", + self.verification_time().as_micros() as f64 / 1_000f64 + )); + str + } + + /// Write a CSV report + pub fn report_csv(&self, w: &mut W) -> std::io::Result<()> { + let mut w = csv::Writer::from_writer(w); + w.write_record(&["max_deg", &self.max_deg.to_string()])?; + w.write_record(&["advice_columns", &self.advice_columns.to_string()])?; + w.write_record(&["lookups", &self.lookups.to_string()])?; + w.write_record(&["permutations", &{ + let mut str = String::new(); + for p in self.permutations.iter() { + str.push_str(&format!(" {}", p.columns)); + } + str + }])?; + w.write_record(&["column_queries", &self.column_queries.to_string()])?; + w.write_record(&["point_sets", &self.point_sets.to_string()])?; + w.write_record(&["proof_size", &self.proof_size().to_string()])?; + w.write_record(&[ + "verification_time", + &(self.verification_time().as_micros() as f64 / 1_000f64).to_string(), + ])?; + Ok(()) + } +} + +/// Given a Plonk circuit, this function returns a [ModelCircuit] +pub fn from_circuit_to_model_circuit< + F: Ord + Field + FromUniformBytes<64>, + C: Circuit, +>( + k: u32, + circuit: &C, + instances: Vec>, +) -> ModelCircuit { + let options = from_circuit_to_cost_model_options(k, circuit, instances); + ModelCircuit::from(options) +} + +/// Given a Plonk circuit, this function returns [CostOptions] +pub fn from_circuit_to_cost_model_options< + F: Ord + Field + FromUniformBytes<64>, + C: Circuit, +>( + k: u32, + circuit: &C, + instances: Vec>, +) -> CostOptions { + let prover = MockProver::run(k, circuit, instances).unwrap(); + let cs = prover.cs; + + let fixed = { + // init the fixed polynomials with no rotations + let mut fixed = vec![Poly { rotations: vec![] }; cs.num_fixed_columns()]; + for (col, rot) in cs.fixed_queries() { + fixed[col.index()].rotations.push(rot.0 as isize); + } + fixed + }; + + let advice = { + // init the advice polynomials with no rotations + let mut advice = vec![Poly { rotations: vec![] }; cs.num_advice_columns()]; + for (col, rot) in cs.advice_queries() { + advice[col.index()].rotations.push(rot.0 as isize); + } + advice + }; + + let instance = { + // init the instance polynomials with no rotations + let mut instance = vec![Poly { rotations: vec![] }; cs.num_instance_columns()]; + for (col, rot) in cs.instance_queries() { + instance[col.index()].rotations.push(rot.0 as isize); + } + instance + }; + + let lookup = { + let mut lookup = vec![]; + for l in cs.lookups().iter() { + lookup.push(Lookup { + // this isn't actually used for estimation right now, so ignore it. + _columns: 1, + input_deg: l.input_expressions().len(), + table_deg: l.table_expressions().len(), + }); + } + lookup + }; + + let permutation = vec![Permutation { + columns: cs.permutation().get_columns().len(), + }]; + + let gate_degree = cs.degree(); + + let k = prover.k.try_into().unwrap(); + + CostOptions { + advice, + instance, + fixed, + gate_degree, + lookup, + permutation, + k, + } +}