From 3550ab07b1c791e01e61b4dff03607a5343b78de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1igo=20Querejeta=20Azurmendi?= <31273774+iquerejeta@users.noreply.github.com> Date: Sat, 30 Dec 2023 07:49:24 +0100 Subject: [PATCH] Benchmarking tool using cost estimator (#235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Benchmarking tool using cost estimator Co-Authored-By: Iñigo Querejeta Azurmendi * Address review comments * Add example of cost-estimator usage * Cost model example behind feature * Make report generic over field size and commitment scheme * Nits * Use serde for printing the report * Calculate max expression degree Use built-in function to compute cs max_degree * Add cost computation for KZG GWC * Add shuffle argument * Nits * Remove CLI * Remove FromStr impl - not needed without CLI * Fix queries of Lookup * There is only one permutation argument * Remove dependency of halo2_gadgets for cost-estimator example --------- Co-authored-by: Henry Blanchette --- halo2_proofs/Cargo.toml | 8 + halo2_proofs/examples/cost-model.rs | 304 -------------------------- halo2_proofs/examples/proof-size.rs | 101 +++++++++ halo2_proofs/src/dev.rs | 3 + halo2_proofs/src/dev/cost_model.rs | 323 ++++++++++++++++++++++++++++ 5 files changed, 435 insertions(+), 304 deletions(-) delete mode 100644 halo2_proofs/examples/cost-model.rs create mode 100644 halo2_proofs/examples/proof-size.rs create mode 100644 halo2_proofs/src/dev/cost_model.rs diff --git a/halo2_proofs/Cargo.toml b/halo2_proofs/Cargo.toml index cf62f69ce0..70750de276 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 } +serde = { version = "1", optional = true, features = ["derive"] } +serde_derive = { version = "1", optional = true} # Developer tooling dependencies plotters = { version = "0.3.0", default-features = false, optional = true } @@ -72,6 +74,7 @@ criterion = "0.3" gumdrop = "0.8" proptest = "1" rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } +serde_json = "1" [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies] getrandom = { version = "0.2", features = ["js"] } @@ -91,6 +94,7 @@ thread-safe-region = [] sanity-checks = [] batch = ["rand_core/getrandom"] circuit-params = [] +cost-estimator = ["serde", "serde_derive"] [lib] bench = false @@ -98,3 +102,7 @@ bench = false [[example]] name = "circuit-layout" required-features = ["test-dev-graph"] + +[[example]] +name = "proof-size" +required-features = ["cost-estimator"] diff --git a/halo2_proofs/examples/cost-model.rs b/halo2_proofs/examples/cost-model.rs deleted file mode 100644 index 100f047fac..0000000000 --- a/halo2_proofs/examples/cost-model.rs +++ /dev/null @@ -1,304 +0,0 @@ -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) - } -} - -#[derive(Debug, Options)] -struct CostOptions { - #[options(help = "Print this message.")] - help: bool, - - #[options( - help = "An advice column with the given rotations. May be repeated.", - meta = "R[,R..]" - )] - advice: Vec, - - #[options( - help = "An instance column with the given rotations. May be repeated.", - meta = "R[,R..]" - )] - instance: Vec, - - #[options( - help = "A fixed column with the given rotations. May be repeated.", - meta = "R[,R..]" - )] - fixed: Vec, - - #[options(help = "Maximum degree of the custom gates.", meta = "D")] - gate_degree: usize, - - #[options( - help = "A lookup over N columns with max input degree I and max table degree T. May be repeated.", - meta = "N,I,T" - )] - lookup: Vec, - - #[options(help = "A permutation over N columns. May be repeated.", meta = "N")] - permutation: Vec, - - #[options(free, required, help = "2^K bound on the number of rows.")] - 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 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); - println!("{:#?}", c); - println!("Proof size: {} bytes", c.proof_size()); - println!( - "Verification: at least {}ms", - c.verification_time().as_micros() as f64 / 1_000f64 - ); -} diff --git a/halo2_proofs/examples/proof-size.rs b/halo2_proofs/examples/proof-size.rs new file mode 100644 index 0000000000..f2b3cf7322 --- /dev/null +++ b/halo2_proofs/examples/proof-size.rs @@ -0,0 +1,101 @@ +use ff::Field; +use halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner, Value}, + plonk::{Advice, Circuit, Column, ConstraintSystem, Error}, +}; +use halo2curves::pasta::Fp; + +use halo2_proofs::dev::cost_model::{from_circuit_to_model_circuit, CommitmentScheme}; +use halo2_proofs::plonk::{Expression, Selector, TableColumn}; +use halo2_proofs::poly::Rotation; + +// We use a lookup example +#[derive(Clone, Copy)] +struct TestCircuit {} + +#[derive(Debug, Clone)] +struct MyConfig { + selector: Selector, + table: TableColumn, + advice: Column, +} + +impl Circuit for TestCircuit { + type Config = MyConfig; + type FloorPlanner = SimpleFloorPlanner; + #[cfg(feature = "circuit-params")] + type Params = (); + + fn without_witnesses(&self) -> Self { + Self {} + } + + fn configure(meta: &mut ConstraintSystem) -> MyConfig { + let config = MyConfig { + selector: meta.complex_selector(), + table: meta.lookup_table_column(), + advice: meta.advice_column(), + }; + + meta.lookup("lookup", |meta| { + let selector = meta.query_selector(config.selector); + let not_selector = Expression::Constant(Fp::ONE) - selector.clone(); + let advice = meta.query_advice(config.advice, Rotation::cur()); + vec![(selector * advice + not_selector, config.table)] + }); + + config + } + + fn synthesize(&self, config: MyConfig, mut layouter: impl Layouter) -> Result<(), Error> { + layouter.assign_table( + || "8-bit table", + |mut table| { + for row in 0u64..(1 << 8) { + table.assign_cell( + || format!("row {}", row), + config.table, + row as usize, + || Value::known(Fp::from(row + 1)), + )?; + } + + Ok(()) + }, + )?; + + layouter.assign_region( + || "assign values", + |mut region| { + for offset in 0u64..(1 << 10) { + config.selector.enable(&mut region, offset as usize)?; + region.assign_advice( + || format!("offset {}", offset), + config.advice, + offset as usize, + || Value::known(Fp::from((offset % 256) + 1)), + )?; + } + + Ok(()) + }, + ) + } +} + +const K: u32 = 11; + +fn main() { + let circuit = TestCircuit {}; + + let model = from_circuit_to_model_circuit::<_, _, 56, 56>( + K, + &circuit, + vec![], + CommitmentScheme::KZGGWC, + ); + println!( + "Cost of circuit with 8 bit lookup table: \n{}", + serde_json::to_string_pretty(&model).unwrap() + ); +} diff --git a/halo2_proofs/src/dev.rs b/halo2_proofs/src/dev.rs index 38c805b085..48dcca5ba0 100644 --- a/halo2_proofs/src/dev.rs +++ b/halo2_proofs/src/dev.rs @@ -36,6 +36,9 @@ pub use failure::{FailureLocation, VerifyFailure}; pub mod cost; pub use cost::CircuitCost; +#[cfg(feature = "cost-estimator")] +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..51b3a1ad76 --- /dev/null +++ b/halo2_proofs/src/dev/cost_model.rs @@ -0,0 +1,323 @@ +//! The cost estimator takes high-level parameters for a circuit design, and estimates the +//! verification cost, as well as resulting proof size. + +use std::collections::HashSet; +use std::{iter, num::ParseIntError, str::FromStr}; + +use crate::plonk::Circuit; +use ff::{Field, FromUniformBytes}; +use serde::Deserialize; +use serde_derive::Serialize; + +use super::MockProver; + +/// Supported commitment schemes +#[derive(Debug, Eq, PartialEq)] +pub enum CommitmentScheme { + /// Inner Product Argument commitment scheme + IPA, + /// KZG with GWC19 mutli-open strategy + KZGGWC, + /// KZG with BDFG20 mutli-open strategy + KZGSHPLONK, +} + +/// Options to build a circuit specification 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, + + /// Maximum degree of the constraint system. + pub max_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: Permutation, + + /// A shuffle over N columns with max input degree I and max shuffle degree T. May be repeated. + pub shuffle: 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; + +impl Lookup { + fn queries(&self) -> impl Iterator { + // - product commitments at x and \omega x + // - 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, Deserialize, Serialize)] +pub struct Permutation { + columns: usize, +} + +impl Permutation { + 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)) + } +} + +/// Structure holding the [Shuffle] related data for circuit benchmarks. +#[derive(Debug, Clone)] +pub struct Shuffle; + +impl Shuffle { + fn queries(&self) -> impl Iterator { + // Open shuffle product commitment at x and \omega x + let shuffle = "0, 1".parse().unwrap(); + + iter::empty().chain(Some(shuffle)) + } +} + +/// High-level specifications of an abstract circuit. +#[derive(Debug, Deserialize, Serialize)] +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 enabled columns. + pub permutations: usize, + /// Number of shuffle arguments + pub shuffles: usize, + /// 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, + /// Size of the proof for the circuit + pub size: usize, +} + +impl CostOptions { + /// Convert [CostOptions] to [ModelCircuit]. The proof sizè is computed depending on the base + /// and scalar field size of the curve used, together with the [CommitmentScheme]. + pub fn into_model_circuit( + &self, + comm_scheme: CommitmentScheme, + ) -> ModelCircuit { + let mut queries: Vec<_> = iter::empty() + .chain(self.advice.iter()) + .chain(self.instance.iter()) + .chain(self.fixed.iter()) + .cloned() + .chain(self.lookup.iter().flat_map(|l| l.queries())) + .chain(self.permutation.queries()) + .chain(self.shuffle.iter().flat_map(|s| s.queries())) + .chain(iter::repeat("0".parse().unwrap()).take(self.max_degree - 1)) + .collect(); + + let column_queries = queries.len(); + queries.sort_unstable(); + queries.dedup(); + let point_sets = queries.len(); + + let comp_bytes = |points: usize, scalars: usize| points * COMM + scalars * SCALAR; + + // PLONK: + // - COMM bytes (commitment) per advice column + // - 3 * COMM bytes (commitments) + 5 * SCALAR bytes (evals) per lookup column + // - COMM bytes (commitment) + 2 * SCALAR bytes (evals) per permutation argument + // - COMM bytes (eval) per column per permutation argument + let plonk = comp_bytes(1, 0) * self.advice.len() + + comp_bytes(3, 5) * self.lookup.len() + + comp_bytes(1, 2 + self.permutation.columns); + + // Vanishing argument: + // - (max_deg - 1) * COMM bytes (commitments) + (max_deg - 1) * SCALAR bytes (h_evals) + // for quotient polynomial + // - SCALAR bytes (eval) per column query + let vanishing = + comp_bytes(self.max_degree - 1, self.max_degree - 1) + comp_bytes(0, column_queries); + + // Multiopening argument: + // - f_commitment (COMM bytes) + // - SCALAR bytes (evals) per set of points in multiopen argument + let multiopen = comp_bytes(1, point_sets); + + let polycomm = match comm_scheme { + CommitmentScheme::IPA => { + // Polycommit IPA: + // - s_poly commitment (COMM bytes) + // - inner product argument (k rounds * 2 * COMM bytes) + // - a (SCALAR bytes) + // - xi (SCALAR bytes) + comp_bytes(1 + 2 * self.k, 2) + } + CommitmentScheme::KZGGWC => { + let mut nr_rotations = HashSet::new(); + for poly in self.advice.iter() { + nr_rotations.extend(poly.rotations.clone()); + } + for poly in self.fixed.iter() { + nr_rotations.extend(poly.rotations.clone()); + } + for poly in self.instance.iter() { + nr_rotations.extend(poly.rotations.clone()); + } + + // Polycommit GWC: + // - number_rotations * COMM bytes + comp_bytes(nr_rotations.len(), 0) + } + CommitmentScheme::KZGSHPLONK => { + // Polycommit SHPLONK: + // - quotient polynomial commitment (COMM bytes) + comp_bytes(1, 0) + } + }; + + let size = plonk + vanishing + multiopen + polycomm; + + ModelCircuit { + k: self.k, + max_deg: self.max_degree, + advice_columns: self.advice.len(), + lookups: self.lookup.len(), + permutations: self.permutation.columns, + shuffles: self.shuffle.len(), + column_queries, + point_sets, + size, + } + } +} + +/// Given a Plonk circuit, this function returns a [ModelCircuit] +pub fn from_circuit_to_model_circuit< + F: Ord + Field + FromUniformBytes<64>, + C: Circuit, + const COMM: usize, + const SCALAR: usize, +>( + k: u32, + circuit: &C, + instances: Vec>, + comm_scheme: CommitmentScheme, +) -> ModelCircuit { + let options = from_circuit_to_cost_model_options(k, circuit, instances); + options.into_model_circuit::(comm_scheme) +} + +/// Given a Plonk circuit, this function returns [CostOptions] +pub fn from_circuit_to_cost_model_options, 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 = { cs.lookups().iter().map(|_| Lookup).collect::>() }; + + let permutation = Permutation { + columns: cs.permutation().get_columns().len(), + }; + + let shuffle = { cs.shuffles.iter().map(|_| Shuffle).collect::>() }; + + let gate_degree = cs + .gates + .iter() + .flat_map(|gate| gate.polynomials().iter().map(|poly| poly.degree())) + .max() + .unwrap_or(0); + + let k = prover.k.try_into().unwrap(); + + CostOptions { + advice, + instance, + fixed, + gate_degree, + max_degree: cs.degree(), + lookup, + permutation, + shuffle, + k, + } +}