diff --git a/backends/concrete-cpu/noise-model/Cargo.toml b/backends/concrete-cpu/noise-model/Cargo.toml new file mode 100644 index 000000000..c2215fb7a --- /dev/null +++ b/backends/concrete-cpu/noise-model/Cargo.toml @@ -0,0 +1,11 @@ +[package] +# see https://doc.rust-lang.org/cargo/reference/manifest.html +name = "concrete-cpu-noise-model" +version = "0.1.0" +authors = [""] +edition = "2021" + +[dependencies] + +[dev-dependencies] +approx = "0.5" diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise.rs new file mode 100644 index 000000000..780d682ae --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise.rs @@ -0,0 +1,2 @@ +pub mod conversion; +pub mod noise; diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise/conversion.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise/conversion.rs new file mode 100644 index 000000000..548cd6d0c --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise/conversion.rs @@ -0,0 +1,15 @@ +fn modular_variance_variance_ratio(ciphertext_modulus_log: u32) -> f64 { + 2_f64.powi(2 * ciphertext_modulus_log as i32) +} + +pub fn modular_variance_to_variance(modular_variance: f64, ciphertext_modulus_log: u32) -> f64 { + modular_variance / modular_variance_variance_ratio(ciphertext_modulus_log) +} + +pub fn variance_to_modular_variance(variance: f64, ciphertext_modulus_log: u32) -> f64 { + variance * modular_variance_variance_ratio(ciphertext_modulus_log) +} + +pub fn variance_to_std_dev(variance: f64) -> f64 { + variance.sqrt() +} diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise/noise.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise.rs new file mode 100644 index 000000000..4d1efde9c --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise.rs @@ -0,0 +1,9 @@ +pub mod blind_rotate; +pub mod cmux; +pub mod external_product_glwe; +pub mod keyswitch; +pub mod keyswitch_one_bit; +pub mod modulus_switching; +pub mod multi_bit_blind_rotate; +pub mod multi_bit_external_product_glwe; +pub mod private_packing_keyswitch; diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/blind_rotate.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/blind_rotate.rs new file mode 100644 index 000000000..8cb09a892 --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/blind_rotate.rs @@ -0,0 +1,101 @@ +use super::cmux::variance_cmux; + +pub const FFT_SCALING_WEIGHT: f64 = -2.577_224_94; + +/// 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_blind_rotate( + in_lwe_dimension: u64, + out_glwe_dimension: u64, + out_polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_bsk: f64, +) -> f64 { + in_lwe_dimension as f64 + * variance_cmux( + out_glwe_dimension, + out_polynomial_size, + log2_base, + level, + ciphertext_modulus_log, + variance_bsk, + ) +} + +#[cfg(test)] +mod tests { + use crate::gaussian_noise::{ + conversion::variance_to_modular_variance, security::minimal_variance_glwe, + }; + + use super::*; + + #[test] + fn security_variance_bootstrap_1() { + let ref_modular_variance = 4.078_296_369_990_673e31; + + let polynomial_size = 1 << 12; + let glwe_dimension = 2; + + let ciphertext_modulus_log = 64; + let security = 128; + let variance_bsk = minimal_variance_glwe( + glwe_dimension, + polynomial_size, + ciphertext_modulus_log, + security, + ); + + let actual = variance_blind_rotate( + 2048, + glwe_dimension, + polynomial_size, + 24, + 2, + ciphertext_modulus_log, + variance_bsk, + ); + + approx::assert_relative_eq!( + variance_to_modular_variance(actual, ciphertext_modulus_log), + ref_modular_variance, + max_relative = 1e-8 + ); + } + + #[test] + fn golden_python_prototype_security_variance_bootstrap_2() { + // golden value include fft correction + let golden_modular_variance = 3.269_722_907_894_341e55; + + let polynomial_size = 1 << 12; + let glwe_dimension = 4; + + let ciphertext_modulus_log = 128; + let security = 128; + let variance_bsk = minimal_variance_glwe( + glwe_dimension, + polynomial_size, + ciphertext_modulus_log, + security, + ); + + let actual = variance_blind_rotate( + 1024, + glwe_dimension, + polynomial_size, + 5, + 9, + ciphertext_modulus_log, + variance_bsk, + ); + + approx::assert_relative_eq!( + variance_to_modular_variance(actual, ciphertext_modulus_log), + golden_modular_variance, + max_relative = 1e-8 + ); + } +} diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/cmux.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/cmux.rs new file mode 100644 index 000000000..23791d7fc --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/cmux.rs @@ -0,0 +1,20 @@ +use super::external_product_glwe::variance_external_product_glwe; + +// only valid in the blind rotate case +pub fn variance_cmux( + glwe_dimension: u64, + polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ggsw: f64, +) -> f64 { + variance_external_product_glwe( + glwe_dimension, + polynomial_size, + log2_base, + level, + ciphertext_modulus_log, + variance_ggsw, + ) +} diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/external_product_glwe.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/external_product_glwe.rs new file mode 100644 index 000000000..0c5725cf5 --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/external_product_glwe.rs @@ -0,0 +1,83 @@ +use crate::{gaussian_noise::conversion::modular_variance_to_variance, utils::square}; + +pub fn variance_external_product_glwe( + glwe_dimension: u64, + polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ggsw: f64, +) -> f64 { + theoretical_variance_external_product_glwe( + glwe_dimension, + polynomial_size, + log2_base, + level, + ciphertext_modulus_log, + variance_ggsw, + ) + fft_noise_variance_external_product_glwe( + glwe_dimension, + polynomial_size, + log2_base, + level, + ciphertext_modulus_log, + ) +} + +fn theoretical_variance_external_product_glwe( + glwe_dimension: u64, + polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ggsw: f64, +) -> f64 { + let variance_key_coefficient_binary: f64 = + modular_variance_to_variance(1. / 4., ciphertext_modulus_log); + + let square_expectation_key_coefficient_binary: f64 = + modular_variance_to_variance(square(1. / 2.), ciphertext_modulus_log); + + let k = glwe_dimension as f64; + let b = 2_f64.powi(log2_base as i32); + let b2l = 2_f64.powi((log2_base * 2 * level) as i32); + let l = level as f64; + let big_n = polynomial_size as f64; + let q_square = 2_f64.powi(2 * ciphertext_modulus_log as i32); + + let res_1 = l * (k + 1.) * big_n * (square(b) + 2.) / 12. * variance_ggsw; + let res_2 = (q_square - b2l) / (24. * b2l) + * (modular_variance_to_variance(1., ciphertext_modulus_log) + + k * big_n + * (variance_key_coefficient_binary + square_expectation_key_coefficient_binary)) + + k * big_n / 8. * variance_key_coefficient_binary + + 1. / 16. * square(1. - k * big_n) * square_expectation_key_coefficient_binary; + + res_1 + res_2 +} + +const FFT_SCALING_WEIGHT: f64 = -2.577_224_94; + +/// Additional noise generated by fft computation + +fn fft_noise_variance_external_product_glwe( + glwe_dimension: u64, + polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, +) -> f64 { + // https://github.com/zama-ai/concrete-optimizer/blob/prototype/python/optimizer/noise_formulas/bootstrap.py#L25 + let b = 2_f64.powi(log2_base as i32); + let l = level as f64; + let big_n = polynomial_size as f64; + let k = glwe_dimension; + assert!(k > 0, "k = {k}"); + assert!(k < 7, "k = {k}"); + + // 22 = 2 x 11, 11 = 64 -53 + let scale_margin = (1_u64 << 22) as f64; + let res = + f64::exp2(FFT_SCALING_WEIGHT) * scale_margin * l * b * b * big_n.powi(2) * (k as f64 + 1.); + modular_variance_to_variance(res, ciphertext_modulus_log) +} diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/keyswitch.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/keyswitch.rs new file mode 100644 index 000000000..e1cad04ba --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/keyswitch.rs @@ -0,0 +1,77 @@ +use super::keyswitch_one_bit::variance_keyswitch_one_bit; + +/// Additional noise generated by the keyswitch step. +pub fn variance_keyswitch( + input_lwe_dimension: u64, //n_big + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ksk: f64, +) -> f64 { + input_lwe_dimension as f64 + * variance_keyswitch_one_bit(log2_base, level, ciphertext_modulus_log, variance_ksk) +} + +#[cfg(test)] +mod tests { + + use crate::gaussian_noise::{ + conversion::variance_to_modular_variance, security::minimal_variance_lwe, + }; + + use super::*; + + #[test] + fn golden_python_prototype_security_variance_keyswitch_1() { + let golden_modular_variance = 5.997_880_135_602_194e68; + let internal_ks_output_lwe_dimension = 1024; + let ciphertext_modulus_log = 128; + let security = 128; + + let actual = variance_keyswitch( + 4096, + 5, + 9, + ciphertext_modulus_log, + minimal_variance_lwe( + internal_ks_output_lwe_dimension, + ciphertext_modulus_log, + security, + ), + ); + + approx::assert_relative_eq!( + variance_to_modular_variance(actual, ciphertext_modulus_log), + 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 = 7.407_691_550_271_225e48; // full estimation + let internal_ks_output_lwe_dimension = 512; + let ciphertext_modulus_log = 64; + let security = 128; + + let actual = variance_keyswitch( + 2048, + 24, + 2, + ciphertext_modulus_log, + minimal_variance_lwe( + internal_ks_output_lwe_dimension, + ciphertext_modulus_log, + security, + ), + ); + + approx::assert_relative_eq!( + variance_to_modular_variance(actual, ciphertext_modulus_log), + golden_modular_variance, + max_relative = 1e-8 + ); + } +} diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/keyswitch_one_bit.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/keyswitch_one_bit.rs new file mode 100644 index 000000000..cbe42fb1e --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/keyswitch_one_bit.rs @@ -0,0 +1,31 @@ +use crate::{gaussian_noise::conversion::modular_variance_to_variance, utils::square}; + +/// Additional noise generated by the bit multiplication +pub fn variance_keyswitch_one_bit( + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ksk: f64, +) -> f64 { + let variance_key_coefficient_binary: f64 = + modular_variance_to_variance(1. / 4., ciphertext_modulus_log); + + let square_expectation_key_coefficient_binary: f64 = + modular_variance_to_variance(square(1. / 2.), ciphertext_modulus_log); + + let base = 2_f64.powi(log2_base as i32); + let b2l = 2_f64.powi((log2_base * 2 * level) as i32); + let q_square = 2_f64.powi((2 * ciphertext_modulus_log) as i32); + + // res 2 + let res_2 = (q_square / (12. * b2l) - 1. / 12.) + * (variance_key_coefficient_binary + square_expectation_key_coefficient_binary); + + // res 3 + let res_3 = 1. / 4. * variance_key_coefficient_binary; + + // res 4 + let res_4 = (level as f64) * variance_ksk * (square(base) + 2.) / 12.; + + res_2 + res_3 + res_4 +} diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/modulus_switching.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/modulus_switching.rs new file mode 100644 index 000000000..2f7fe6907 --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/modulus_switching.rs @@ -0,0 +1,15 @@ +use crate::{gaussian_noise::conversion::modular_variance_to_variance, utils::square}; + +pub fn estimate_modulus_switching_noise_with_binary_key( + internal_ks_output_lwe_dimension: u64, + glwe_log2_polynomial_size: u64, + ciphertext_modulus_log: u32, +) -> f64 { + let nb_msb = glwe_log2_polynomial_size + 1; + + let w = 2_f64.powi(nb_msb as i32); + let n = internal_ks_output_lwe_dimension as f64; + + (1. / 12. + n / 24.) / square(w) + + modular_variance_to_variance(-1. / 12. + n / 48., ciphertext_modulus_log) +} diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/multi_bit_blind_rotate.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/multi_bit_blind_rotate.rs new file mode 100644 index 000000000..3489b5d43 --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/multi_bit_blind_rotate.rs @@ -0,0 +1,34 @@ +use super::multi_bit_external_product_glwe::variance_multi_bit_external_product_glwe; + +/// Final reduced noise generated by the final multi bit bootstrap step. +/// Note that it does not depends from input noise, assuming the bootstrap is successful +#[allow(clippy::too_many_arguments)] +pub fn variance_multi_bit_blind_rotate( + in_lwe_dimension: u64, + out_glwe_dimension: u64, + out_polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_bsk: f64, + grouping_factor: u32, + jit_fft: bool, +) -> f64 { + assert_eq!( + in_lwe_dimension % (grouping_factor as u64), + 0, + "in_lwe_dimension ({in_lwe_dimension}) has \ + to be a multiple of grouping_factor ({grouping_factor})" + ); + (in_lwe_dimension / (grouping_factor as u64)) as f64 + * variance_multi_bit_external_product_glwe( + out_glwe_dimension, + out_polynomial_size, + log2_base, + level, + ciphertext_modulus_log, + variance_bsk, + grouping_factor, + jit_fft, + ) +} diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/multi_bit_external_product_glwe.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/multi_bit_external_product_glwe.rs new file mode 100644 index 000000000..d5137efcf --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/multi_bit_external_product_glwe.rs @@ -0,0 +1,108 @@ +use crate::{gaussian_noise::conversion::modular_variance_to_variance, utils::square}; + +#[allow(clippy::too_many_arguments)] +pub fn variance_multi_bit_external_product_glwe( + glwe_dimension: u64, + polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ggsw: f64, + grouping_factor: u32, + jit_fft: bool, +) -> f64 { + theoretical_variance_multi_bit_external_product_glwe( + glwe_dimension, + polynomial_size, + log2_base, + level, + ciphertext_modulus_log, + variance_ggsw, + grouping_factor, + ) + fft_noise_variance_multi_bit_external_product_glwe( + glwe_dimension, + polynomial_size, + log2_base, + level, + ciphertext_modulus_log, + grouping_factor, + jit_fft, + ) +} + +fn theoretical_variance_multi_bit_external_product_glwe( + glwe_dimension: u64, + polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ggsw: f64, + grouping_factor: u32, +) -> f64 { + let variance_key_coefficient_binary: f64 = + modular_variance_to_variance(1. / 4., ciphertext_modulus_log); + + let square_expectation_key_coefficient_binary: f64 = + modular_variance_to_variance(square(1. / 2.), ciphertext_modulus_log); + + let k = glwe_dimension as f64; + let b = 2_f64.powi(log2_base as i32); + let b2l = 2_f64.powi((log2_base * 2 * level) as i32); + let l = level as f64; + let big_n = polynomial_size as f64; + let q_square = 2_f64.powi(2 * ciphertext_modulus_log as i32); + + let res_1 = l * (k + 1.) * big_n * (square(b) + 2.) / 12. + * variance_ggsw + * 2.0f64.powi(grouping_factor as i32); + let res_2 = (q_square - b2l) / (24. * b2l) + * (modular_variance_to_variance(1., ciphertext_modulus_log) + + k * big_n + * (variance_key_coefficient_binary + square_expectation_key_coefficient_binary)) + + k * big_n / 8. * variance_key_coefficient_binary + + 1. / 16. * square(1. - k * big_n) * square_expectation_key_coefficient_binary; + + res_1 + res_2 +} + +const FFT_SCALING_WEIGHTS: [(u32, f64); 3] = [ + (2, 0.265_753_885_551_084_5), + (3, 1.350_324_550_016_489_8), + (4, 2.475_036_769_207_096), +]; +const JIT_FFT_SCALING_WEIGHT: f64 = -2.015_541_494_298_571_7; + +/// Additional noise generated by fft computation +fn fft_noise_variance_multi_bit_external_product_glwe( + glwe_dimension: u64, + polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + grouping_factor: u32, + jit_fft: bool, +) -> f64 { + let b = 2_f64.powi(log2_base as i32); + let l = level as f64; + let big_n = polynomial_size as f64; + let k = glwe_dimension; + assert!(k > 0, "k = {k}"); + assert!(k < 7, "k = {k}"); + + let fft_scaling_weight = if jit_fft { + JIT_FFT_SCALING_WEIGHT + } else { + let index = FFT_SCALING_WEIGHTS + .binary_search_by_key(&grouping_factor, |&(factor, _)| factor) + .unwrap_or_else(|_| { + panic!("Could not find fft scaling weight for grouping factor {grouping_factor}.") + }); + FFT_SCALING_WEIGHTS[index].1 + }; + + // 22 = 2 x 11, 11 = 64 -53 + let scale_margin = (1_u64 << 22) as f64; + let res = + f64::exp2(fft_scaling_weight) * scale_margin * l * b * b * big_n.powi(2) * (k as f64 + 1.); + modular_variance_to_variance(res, ciphertext_modulus_log) +} diff --git a/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/private_packing_keyswitch.rs b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/private_packing_keyswitch.rs new file mode 100644 index 000000000..5fda38e5c --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/gaussian_noise/noise/private_packing_keyswitch.rs @@ -0,0 +1,38 @@ +use crate::{gaussian_noise::conversion::modular_variance_to_variance, utils::square}; + +// packing private keyswitch for WoP-PBS, described in algorithm 3 of https://eprint.iacr.org/2018/421.pdf (TFHE paper) +pub fn estimate_packing_private_keyswitch( + var_glwe: f64, + var_ggsw: f64, + log2_base: u64, + level: u64, + output_glwe_dimension: u64, + output_polynomial_size: u64, + ciphertext_modulus_log: u32, +) -> f64 { + let variance_key_coefficient_binary: f64 = 1. / 4.; + let expectation_key_coefficient_binary: f64 = 1. / 2.; + + let l = level as f64; + let b = 2f64.powi(log2_base as i32); + let n = (output_glwe_dimension * output_polynomial_size) as f64; // param.internal_lwe_dimension.0 as f64; + let b2l = f64::powi(b, 2 * level as i32); + let var_s_w = 1. / 4.; + let mean_s_w = 1. / 2.; + let res_1 = l * (n + 1.) * var_ggsw * (square(b) + 2.) / 12.; + + #[allow(clippy::cast_possible_wrap)] + let res_3 = (f64::powi(2., 2 * ciphertext_modulus_log as i32) - b2l) / (12. * b2l) + * modular_variance_to_variance( + 1. + n * variance_key_coefficient_binary + square(expectation_key_coefficient_binary), + ciphertext_modulus_log, + ) + + n / 4. + * modular_variance_to_variance(variance_key_coefficient_binary, ciphertext_modulus_log) + + var_glwe * (var_s_w + square(mean_s_w)); + + let res_5 = modular_variance_to_variance(var_s_w, ciphertext_modulus_log) * 1. / 4. + * square(1. - n * expectation_key_coefficient_binary); + + res_1 + res_3 + res_5 +} diff --git a/backends/concrete-cpu/noise-model/src/lib.rs b/backends/concrete-cpu/noise-model/src/lib.rs new file mode 100644 index 000000000..8db3e23b0 --- /dev/null +++ b/backends/concrete-cpu/noise-model/src/lib.rs @@ -0,0 +1,23 @@ +#![warn(clippy::nursery)] +#![warn(clippy::pedantic)] +#![warn(clippy::style)] +#![allow(clippy::cast_lossless)] +#![allow(clippy::cast_precision_loss)] // u64 to f64 +#![allow(clippy::cast_possible_truncation)] // u64 to usize +#![allow(clippy::missing_panics_doc)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::must_use_candidate)] +#![allow(clippy::suboptimal_flops)] +#![allow(clippy::cast_possible_wrap)] +#![warn(unused_results)] + +pub mod gaussian_noise; + +pub(crate) mod utils { + pub fn square(v: V) -> V + where + V: std::ops::Mul + Copy, + { + v * v + } +}