From 847e758a6dadab245ca9205b4a4ebe3271caf76e Mon Sep 17 00:00:00 2001 From: rudy Date: Thu, 17 Feb 2022 17:47:43 +0100 Subject: [PATCH] feat(noise): atomic pattern noise Resolve #10 --- src/lib.rs | 1 + src/noise_estimator/mod.rs | 3 + .../operators/atomic_pattern.rs | 339 ++++++++++++++++++ src/noise_estimator/operators/mod.rs | 1 + src/noise_estimator/security.rs | 94 +++++ src/noise_estimator/utils.rs | 10 + 6 files changed, 448 insertions(+) create mode 100644 src/noise_estimator/mod.rs create mode 100644 src/noise_estimator/operators/atomic_pattern.rs create mode 100644 src/noise_estimator/operators/mod.rs create mode 100644 src/noise_estimator/security.rs create mode 100644 src/noise_estimator/utils.rs diff --git a/src/lib.rs b/src/lib.rs index 969abf402..f5adc687f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,5 +17,6 @@ pub mod computing_cost; pub mod global_parameters; pub mod graph; +pub mod noise_estimator; pub mod parameters; pub mod weight; diff --git a/src/noise_estimator/mod.rs b/src/noise_estimator/mod.rs new file mode 100644 index 000000000..46666d91e --- /dev/null +++ b/src/noise_estimator/mod.rs @@ -0,0 +1,3 @@ +pub mod operators; +pub mod security; +pub mod utils; diff --git a/src/noise_estimator/operators/atomic_pattern.rs b/src/noise_estimator/operators/atomic_pattern.rs new file mode 100644 index 000000000..7d9f13df8 --- /dev/null +++ b/src/noise_estimator/operators/atomic_pattern.rs @@ -0,0 +1,339 @@ +use concrete_commons::dispersion::{DispersionParameter, Variance}; +use concrete_commons::key_kinds::BinaryKeyKind; +use concrete_commons::numeric::UnsignedInteger; +use concrete_commons::parameters::{ + DecompositionBaseLog, DecompositionLevelCount, GlweDimension, LweDimension, PolynomialSize, +}; + +use super::super::security; + +/// Additional noise generated by the keyswitch step. +pub fn variance_keyswitch( + input_lwe_dimension: u64, //n_big + ks_decomposition_level_count: u64, //l(BS) + ks_decomposition_base_log: u64, //b(BS) + ciphertext_modulus_log: u64, + variance_ksk: Variance, +) -> Variance { + assert!(ciphertext_modulus_log == W::BITS as u64); + concrete_npe::estimate_keyswitch_noise_lwe_to_glwe_with_constant_terms::< + W, + Variance, + Variance, + BinaryKeyKind, + >( + LweDimension(input_lwe_dimension as usize), + Variance(0.0), + variance_ksk, + DecompositionBaseLog(ks_decomposition_base_log as usize), + DecompositionLevelCount(ks_decomposition_level_count as usize), + ) +} + +/// Compute the variance paramater for `variance_keyswitch` +pub fn variance_ksk( + internal_ks_output_lwe_dimension: u64, + ciphertext_modulus_log: u64, + security_level: u64, +) -> Variance { + let glwe_poly_size = 1; + let glwe_dim = internal_ks_output_lwe_dimension; + security::variance_ksk( + glwe_poly_size, + glwe_dim, + ciphertext_modulus_log, + security_level, + ) +} + +/// Additional noise generated by fft computation +pub fn fft_noise( + internal_ks_output_lwe_dimension: u64, //n_small + glwe_polynomial_size: u64, //N + _glwe_dimension: u64, //k, unused + br_decomposition_level_count: u64, //l(KS) + br_decomposition_base_log: u64, //b(ks) +) -> Variance { + // https://github.com/zama-ai/concrete-optimizer/blob/prototype/python/optimizer/noise_formulas/bootstrap.py#L25 + let n = internal_ks_output_lwe_dimension as f64; + let b = (1_u64 << br_decomposition_base_log) as f64; + let l = br_decomposition_level_count as f64; + let big_n = glwe_polynomial_size as f64; + // 22 = 2 x 11, 11 = 64 -53 + let scale_margin = (1_u64 << 22) as f64; + let res = n * (0.016089458900501813 * scale_margin * l * b * b * big_n.powf(2.188930746713708)); + Variance::from_modular_variance::(res) +} + +/// Final reduced noise generated by the final bootstrap step. +/// Note that it does not depends from input noise, assuming the bootstrap is successful +pub fn variance_bootstrap( + internal_ks_output_lwe_dimension: u64, //n_small + glwe_polynomial_size: u64, //N + glwe_dimension: u64, //k + br_decomposition_level_count: u64, //l(KS) + br_decomposition_base_log: u64, //b(ks) + ciphertext_modulus_log: u64, + variance_bsk: Variance, +) -> Variance { + assert!(ciphertext_modulus_log == W::BITS as u64); + let out_variance_pbs = concrete_npe::estimate_pbs_noise::( + LweDimension(internal_ks_output_lwe_dimension as usize), + PolynomialSize(glwe_polynomial_size as usize), + GlweDimension(glwe_dimension as usize), + DecompositionBaseLog(br_decomposition_base_log as usize), + DecompositionLevelCount(br_decomposition_level_count as usize), + variance_bsk, + ); + let additional_fft_noise = fft_noise::( + internal_ks_output_lwe_dimension, + glwe_polynomial_size, + glwe_dimension, + br_decomposition_level_count, + br_decomposition_base_log, + ); + Variance(out_variance_pbs.get_variance() + additional_fft_noise.get_variance()) +} + +pub fn estimate_modulus_switching_noise_with_binary_key( + internal_ks_output_lwe_dimension: u64, + glwe_polynomial_size: u64, +) -> Variance +where + W: UnsignedInteger, +{ + #[allow(clippy::cast_sign_loss)] + let nb_msb = (f64::log2(glwe_polynomial_size as f64) as usize) + 1; + concrete_npe::estimate_modulus_switching_noise_with_binary_key::( + LweDimension(internal_ks_output_lwe_dimension as usize), + nb_msb, + Variance(0.0), + ) +} + +pub fn maximal_noise( + input_variance: Variance, + input_lwe_dimension: u64, //n_big + internal_ks_output_lwe_dimension: u64, //n_small + ks_decomposition_level_count: u64, //l(BS) + ks_decomposition_base_log: u64, //b(BS) + glwe_polynomial_size: u64, //N + ciphertext_modulus_log: u64, //log(q) + security_level: u64, +) -> Variance +where + D: DispersionParameter, + W: UnsignedInteger, +{ + assert!(ciphertext_modulus_log == W::BITS as u64); + let v_keyswitch = variance_keyswitch::( + input_lwe_dimension, + ks_decomposition_level_count, + ks_decomposition_base_log, + ciphertext_modulus_log, + variance_ksk( + internal_ks_output_lwe_dimension, + ciphertext_modulus_log, + security_level, + ), + ); + let v_modulus_switch = estimate_modulus_switching_noise_with_binary_key::( + internal_ks_output_lwe_dimension, + glwe_polynomial_size, + ); + Variance( + input_variance.get_variance() + + v_keyswitch.get_variance() + + v_modulus_switch.get_variance(), + ) +} + +/// The maximal noise is attained at the end of the modulus switch. +pub fn maximal_noise_multi_sum( + dispersions: &[D], + weights_tuples: &[(W, Ignored)], + input_lwe_dimension: u64, //n_big + internal_ks_output_lwe_dimension: u64, //n_small + ks_decomposition_level_count: u64, //l(BS) + ks_decomposition_base_log: u64, //b(BS) + glwe_polynomial_size: u64, //N + _glwe_dimension: u64, //k + _br_decomposition_level_count: u64, //l(KS) + _br_decomposition_base_log: u64, //b(ks) + ciphertext_modulus_log: u64, //log(q) + security_level: u64, +) -> Variance +where + D: DispersionParameter, + W: UnsignedInteger, +{ + assert!(ciphertext_modulus_log == W::BITS as u64); + let v_out_multi_sum = if dispersions.is_empty() { + let mut weights = vec![]; + for (weight, _) in weights_tuples.iter() { + weights.push(*weight); + } + concrete_npe::estimate_weighted_sum_noise(dispersions, weights.as_slice()) + } else { + Variance(0.0) + }; + maximal_noise::( + v_out_multi_sum, + input_lwe_dimension, + internal_ks_output_lwe_dimension, + ks_decomposition_level_count, + ks_decomposition_base_log, + glwe_polynomial_size, + ciphertext_modulus_log, + security_level, + ) +} + +/// The output noise is the variance boostrap. +pub fn output_noise( + _input_lwe_dimension: u64, //n_big + internal_ks_output_lwe_dimension: u64, //n_small + _ks_decomposition_level_count: u64, //l(BS) + _ks_decomposition_base_log: u64, //b(BS) + glwe_polynomial_size: u64, //N + glwe_dimension: u64, //k + br_decomposition_level_count: u64, //l(KS) + br_decomposition_base_log: u64, //b(ks) + ciphertext_modulus_log: u64, + security_level: u64, +) -> Variance +where + D: DispersionParameter, + W: UnsignedInteger, +{ + let variance_bsk = security::variance_bsk( + glwe_polynomial_size, + glwe_dimension, + ciphertext_modulus_log, + security_level, + ); + variance_bootstrap::( + internal_ks_output_lwe_dimension, + glwe_polynomial_size, + glwe_dimension, + br_decomposition_level_count, + br_decomposition_base_log, + ciphertext_modulus_log, + variance_bsk, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn golden_python_prototype_security_variance_keyswitch_1() { + let golden_modular_variance = 3.260702274017557e+68; + let input_lwe_dimension = 4096; + let internal_ks_output_lwe_dimension = 1024; + let ks_decomposition_level_count = 9; + let ks_decomposition_base_log = 5; + let ciphertext_modulus_log = 128; + let security = 128; + let actual = variance_keyswitch::( + input_lwe_dimension, + ks_decomposition_level_count, + ks_decomposition_base_log, + ciphertext_modulus_log, + variance_ksk( + internal_ks_output_lwe_dimension, + ciphertext_modulus_log, + security, + ), + ) + .get_modular_variance::(); + approx::assert_relative_eq!(actual, golden_modular_variance, max_relative = 1e-8); + } + + #[test] + fn golden_python_prototype_security_variance_keyswitch_2() { + // let golden_modular_variance = 8.580795457940938e+66; + // the full npe implements a part of the full estimation + let golden_modular_variance = 3.941898681369209e+48; // full estimation + let input_lwe_dimension = 2048; + let internal_ks_output_lwe_dimension = 512; + let ks_decomposition_level_count = 2; + let ks_decomposition_base_log = 24; + let ciphertext_modulus_log = 64; + let security = 128; + let actual = variance_keyswitch::( + input_lwe_dimension, + ks_decomposition_level_count, + ks_decomposition_base_log, + ciphertext_modulus_log, + variance_ksk( + internal_ks_output_lwe_dimension, + ciphertext_modulus_log, + security, + ), + ) + .get_modular_variance::(); + approx::assert_relative_eq!(actual, golden_modular_variance, max_relative = 1e-8); + } + + #[test] + fn golden_python_prototype_security_variance_bootstrap_1() { + // golden value include fft correction + let golden_modular_variance = 6.283575623979502e+30; + let internal_ks_output_lwe_dimension = 2048; + let glwe_polynomial_size = 4096; + let glwe_dimension = 10; + let br_decomposition_level_count = 2; + let br_decomposition_base_log = 24; + let ciphertext_modulus_log = 64; + let security = 128; + let variance_bsk = security::variance_bsk( + glwe_polynomial_size, + glwe_dimension, + ciphertext_modulus_log, + security, + ); + let actual = variance_bootstrap::( + internal_ks_output_lwe_dimension, + glwe_polynomial_size, + glwe_dimension, + br_decomposition_level_count, + br_decomposition_base_log, + ciphertext_modulus_log, + variance_bsk, + ) + .get_modular_variance::(); + approx::assert_relative_eq!(actual, golden_modular_variance, max_relative = 1e-8); + } + + #[test] + fn golden_python_prototype_security_variance_bootstrap_2() { + // golden value include fft correction + let golden_modular_variance = 1.3077694369436019e+56; + let internal_ks_output_lwe_dimension = 1024; + let glwe_polynomial_size = 4096; + let glwe_dimension = 16; + let br_decomposition_level_count = 9; + let br_decomposition_base_log = 5; + let ciphertext_modulus_log = 128; + let security = 128; + let variance_bsk = security::variance_bsk( + glwe_polynomial_size, + glwe_dimension, + ciphertext_modulus_log, + security, + ); + let actual = variance_bootstrap::( + internal_ks_output_lwe_dimension, + glwe_polynomial_size, + glwe_dimension, + br_decomposition_level_count, + br_decomposition_base_log, + ciphertext_modulus_log, + variance_bsk, + ) + .get_modular_variance::(); + approx::assert_relative_eq!(actual, golden_modular_variance, max_relative = 1e-8); + } +} diff --git a/src/noise_estimator/operators/mod.rs b/src/noise_estimator/operators/mod.rs new file mode 100644 index 000000000..cb66c1737 --- /dev/null +++ b/src/noise_estimator/operators/mod.rs @@ -0,0 +1 @@ +pub mod atomic_pattern; diff --git a/src/noise_estimator/security.rs b/src/noise_estimator/security.rs new file mode 100644 index 000000000..743d92245 --- /dev/null +++ b/src/noise_estimator/security.rs @@ -0,0 +1,94 @@ +use concrete_commons::dispersion::Variance; + +/// Noise ensuring security +// It was 128 bits of security on the 30th August 2021 with https://bitbucket.org/malb/lwe-estimator/commits/fb7deba98e599df10b665eeb6a26332e43fb5004 +pub fn variance_glwe( + glwe_polynomial_size: u64, + glwe_dimension: u64, + ciphertext_modulus_log: u64, + security_level: u64, +) -> Variance { + // https://github.com/zama-ai/concrete-optimizer/blob/prototype/python/optimizer/noise_formulas/security.py + // ensure to have a minimal on std deviation covering the 2 lowest bits on modular scale + if security_level != 128 { + panic!("Only 128 bits of security is supported") + } + let espsilon_log2_std_modular = 2.0; + let espsilon_log2_std = espsilon_log2_std_modular - (ciphertext_modulus_log as f64); + let equiv_lwe_dimension = (glwe_dimension * glwe_polynomial_size) as f64; + let secure_log2_std = -0.026374888765705498 * equiv_lwe_dimension + 2.012143923330495; + // TODO: could be added instead + let log2_std = f64::max(secure_log2_std, espsilon_log2_std); + let log2_var = 2.0 * log2_std; + Variance(f64::exp2(log2_var)) +} + +/// Noise ensuring ksk security +pub fn variance_ksk( + glwe_polynomial_size: u64, + glwe_dimension: u64, + ciphertext_modulus_log: u64, + security_level: u64, +) -> Variance { + // https://github.com/zama-ai/concrete-optimizer/blob/prototype/python/optimizer/noise_formulas/keyswitch.py#L13 + variance_glwe( + glwe_polynomial_size, + glwe_dimension, + ciphertext_modulus_log, + security_level, + ) +} + +/// Noise ensuring bsk security +pub fn variance_bsk( + glwe_polynomial_size: u64, + glwe_dimension: u64, + ciphertext_modulus_log: u64, + security_level: u64, +) -> Variance { + // https://github.com/zama-ai/concrete-optimizer/blob/prototype/python/optimizer/noise_formulas/bootstrap.py#L66 + variance_glwe( + glwe_polynomial_size, + glwe_dimension, + ciphertext_modulus_log, + security_level, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use concrete_commons::dispersion::DispersionParameter; + + #[test] + fn golden_python_prototype_security_variance_glwe_low() { + // python securityFunc(10,14,64)= 0.3120089883926036 + let log_poly_size = 14; + let glwe_dimension = 10; + let integer_size = 64; + let golden_std_dev = 0.312_008_988_392_6036; + let security_level = 128; + let actual = variance_glwe(log_poly_size, glwe_dimension, integer_size, security_level); + approx::assert_relative_eq!( + golden_std_dev, + actual.get_standard_dev(), + epsilon = f64::EPSILON + ); + } + + #[test] + fn golden_python_prototype_security_variance_glwe_high() { + // python securityFunc(3,8,32)= 2.6011445832514504 + let log_poly_size = 8; + let glwe_dimension = 3; + let integer_size = 32; + let golden_std_dev = 2.6011445832514504; + let security_level = 128; + let actual = variance_glwe(log_poly_size, glwe_dimension, integer_size, security_level); + approx::assert_relative_eq!( + golden_std_dev, + actual.get_standard_dev(), + epsilon = f64::EPSILON + ); + } +} diff --git a/src/noise_estimator/utils.rs b/src/noise_estimator/utils.rs new file mode 100644 index 000000000..0064e4610 --- /dev/null +++ b/src/noise_estimator/utils.rs @@ -0,0 +1,10 @@ +use concrete_commons::dispersion::Variance; + +pub fn from_modular_variance(modular_variance: f64, ciphertext_modulus_log: u64) -> Variance { + match ciphertext_modulus_log { + 128 => Variance::from_modular_variance::(modular_variance), + 64 => Variance::from_modular_variance::(modular_variance), + 32 => Variance::from_modular_variance::(modular_variance), + _ => panic!(), + } +}