feat: add MetaParameterFinder

This commit is contained in:
Thomas Montaigu
2025-12-09 10:41:57 +01:00
committed by tmontaigu
parent be1de6ef2b
commit aa49d141c7
6 changed files with 160 additions and 74 deletions

View File

@@ -2,9 +2,10 @@
This document explains how the choice of cryptographic parameters impacts both the security and efficiency of FHE algorithms. The chosen parameters determine the error probability (sometimes referred to failure probability) and overall performance of computations using fully homomorphic encryption. This error probability is due to the noisy nature of FHE computations (see [here](../../getting-started/security-and-cryptography.md) for more details about the encryption process).
All parameter sets provide at least 128-bits of security according to the [Lattice-Estimator](https://github.com/malb/lattice-estimator).
All parameter sets provide at least 128-bits of security according to the [Lattice-Estimator](https://github.com/malb/lattice-estimator).
## Default parameters
Currently, the default parameters use blocks that contain 2 bits of message and 2 bits of carry - a tweaked uniform (TUniform, defined [here](../../getting-started/security-and-cryptography.md#noise)) noise distribution, and have a bootstrapping failure probability $$p_{error} \le 2^{-128}$$.
These are particularly suitable for applications that need to be secure in the IND-CPA^D model (see [here](../../getting-started/security-and-cryptography.md#security) for more details).
@@ -34,6 +35,7 @@ Parameter sets are versioned for backward compatibility. This means that each se
All parameter sets are stored as variables inside the `tfhe::shortint::parameters` module, with submodules named after the versions of **TFHE-rs** in which these parameters where added. For example, parameters added in **TFHE-rs** v1.0 can be found inside `tfhe::shortint::parameters::v1_0`.
The naming convention of these parameters indicates their capabilities. Taking `tfhe::parameters::v1_0::V1_0_PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128` as an example:
- `V1_0`: these parameters were introduced in **TFHE-rs** v1.0
- `MESSAGE_2`: LWE blocks include 2 bits of message
- `CARRY_2`: LWE blocks include 2 bits of carry
@@ -43,8 +45,69 @@ The naming convention of these parameters indicates their capabilities. Taking `
For convenience, aliases are provided for the most used sets of parameters and stored in the module `tfhe::shortint::parameters::aliases`. Note, however, that these parameters are not stable over time and are always updated to the latest **TFHE-rs** version. For this reason, they should only be used for prototyping and are not suitable for production use cases.
## How to choose the parameter sets
Since tfhe-rs 1.5, there is a `MetaParameterFinder` which enables to search for suitable parameters given some choice of constraints.
### Note
It is recommended to serialize the parameters found if you plan on re-using them, as the heuristics used by the `MetaParametersFinder` are susceptible to change across TFHE-rs versions
```rust
use tfhe::shortint::parameters::{MetaParametersFinder, Log2PFail, Constraint, Version, Backend, NoiseDistributionChoice, NoiseDistributionKind};
use tfhe::{FheUint32, CompressedCiphertextListBuilder, generate_keys, set_server_key};
use tfhe::prelude::*;
fn main() {
// Create a finder with minimal constraints
let finder = MetaParametersFinder::new(
// We want parameters that have a failure probability `pfail` that is: pfail <= 2^-64
Constraint::LessThanOrEqual(Log2PFail(-64.0)),
// We want parameters meant for CPU execution
Backend::Cpu
);
let parameters = finder
.find()
.expect("Could not find suitable parameters");
// It is recommended to serialize the parameters found if you plan on re-using them
let mut serialized_params = vec![];
tfhe::safe_serialization::safe_serialize(&parameters, &mut serialized_params, 1 << 10).unwrap();
let (client_key, server_key) = generate_keys(parameters);
// We can add other constraints:
let finder = finder
// Find parameters from the 1.4 version
// By default, the finder looks in the current tfhe-rs version
.with_version(Version(1, 4))
// We want to use compression (CompressedCiphertextList)
// So we require parameters that support it
.with_compression(true)
.with_noise_distribution(
// Allow any noise distribution that is not TUniform
NoiseDistributionChoice::allow_all()
.deny(NoiseDistributionKind::TUniform)
);
let parameters = finder
.find()
.expect("Could not find suitable parameters");
let (client_key, server_key) = generate_keys(parameters);
let a = FheUint32::encrypt(1337u32, &client_key);
set_server_key(server_key);
let compressed_list = CompressedCiphertextListBuilder::new()
.push(a)
.build()
.unwrap();
}
```
You can override the default parameters with the `with_custom_parameters(block_parameters)` method of the `Config` object. For example, to use a Gaussian distribution instead of the TUniform one, you can modify your configuration as follows:
```rust

View File

@@ -49,18 +49,19 @@ To do so, you need to build a list containing all the ciphertexts that have to b
There is no constraint regarding the size of the list.
There are two possible approaches:
- **Single list**: Compressing several ciphertexts into a single list. This generally yields a better compression ratio between output and input sizes;
- **Multiple lists**: Using multiple lists. This offers more flexibility, since compression might happen at different times in the code, but could lead to larger outputs.
In more details, the optimal ratio is achieved with a list whose size is
equal to the `lwe_per_glwe` field from the `CompressionParameters`.
In more details, the optimal ratio is achieved with a list whose size is
equal to the `lwe_per_glwe` field from the `CompressionParameters`.
The following example shows how to compress and decompress a list containing 4 messages: one 32-bits integer, one 64-bit integer, one boolean, and one 2-bit integer.
```rust
use tfhe::prelude::*;
use tfhe::shortint::parameters::{
COMP_PARAM_MESSAGE_2_CARRY_2, PARAM_MESSAGE_2_CARRY_2,
MetaParametersFinder, Log2PFail, Constraint, Backend,
};
use tfhe::{
set_server_key, CompressedCiphertextList, CompressedCiphertextListBuilder, FheBool,
@@ -68,12 +69,15 @@ use tfhe::{
};
fn main() {
let config =
tfhe::ConfigBuilder::with_custom_parameters(PARAM_MESSAGE_2_CARRY_2)
.enable_compression(COMP_PARAM_MESSAGE_2_CARRY_2)
.build();
let parameters = MetaParametersFinder::new(
Constraint::LessThanOrEqual(Log2PFail(-128.0)),
Backend::Cpu
)
.with_compression(true)
.find()
.expect("Could not find suitable parameters");
let ck = tfhe::ClientKey::generate(config);
let ck = tfhe::ClientKey::generate(parameters);
let sk = tfhe::ServerKey::new(&ck);
set_server_key(sk);

View File

@@ -168,7 +168,7 @@ impl From<MetaParameters> for Config {
.and_then(|ns_p| ns_p.compression_parameters),
cpk_re_randomization_ksk_params: meta_params
.dedicated_compact_public_key_parameters
.and_then(|dedicated_pke| dedicated_pke.re_randomization_parameters),
.and_then(|params| params.re_randomization_parameters),
},
}
}

View File

@@ -173,4 +173,8 @@ impl ClassicPBSParameters {
degree,
}
}
pub const fn pbs_order(&self) -> PBSOrder {
self.encryption_key_choice.into_pbs_order()
}
}

View File

@@ -11,9 +11,12 @@ use crate::shortint::backward_compatibility::parameters::{
};
use crate::shortint::parameters::{
Backend, CompactPublicKeyEncryptionParameters, CompressionParameters,
MetaNoiseSquashingParameters, ShortintKeySwitchingParameters,
MetaNoiseSquashingParameters, ShortintKeySwitchingParameters, SupportedCompactPkeZkScheme,
};
use crate::shortint::{
AtomicPatternParameters, CarryModulus, EncryptionKeyChoice, MessageModulus,
MultiBitPBSParameters, PBSParameters,
};
use crate::shortint::AtomicPatternParameters;
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize, Versionize)]
#[versionize(DedicatedCompactPublicKeyParametersVersions)]
@@ -199,24 +202,6 @@ impl Version {
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let major_cmp = self.major().cmp(&other.major());
if major_cmp != std::cmp::Ordering::Equal {
return major_cmp;
}
self.minor().cmp(&other.minor())
}
}
/// Allows specification of constraints for the multi-bit PBS
pub struct MultiBitPBSChoice {
pub(crate) grouping_factor: Constraint<usize>,
@@ -369,7 +354,7 @@ impl Default for CompactPkeZkSchemeChoice {
}
#[derive(Copy, Clone, Debug)]
pub struct PkeSwitchChoice(EnumSet<EncryptionKeyChoice>);
pub struct PkeKeyswitchTargetChoice(EnumSet<EncryptionKeyChoice>);
impl CastInto<usize> for EncryptionKeyChoice {
fn cast_into(self) -> usize {
@@ -377,7 +362,7 @@ impl CastInto<usize> for EncryptionKeyChoice {
}
}
impl PkeSwitchChoice {
impl PkeKeyswitchTargetChoice {
pub fn new() -> Self {
Self(EnumSet::new())
}
@@ -403,7 +388,7 @@ impl PkeSwitchChoice {
}
}
impl Default for PkeSwitchChoice {
impl Default for PkeKeyswitchTargetChoice {
fn default() -> Self {
Self::new()
}
@@ -412,7 +397,7 @@ impl Default for PkeSwitchChoice {
/// Constraints for the dedicated compact public key
pub struct DedicatedPublicKeyChoice {
zk_scheme: CompactPkeZkSchemeChoice,
pke_switch: PkeSwitchChoice,
pke_keyswitch_target: PkeKeyswitchTargetChoice,
require_re_rand: bool,
}
@@ -428,8 +413,8 @@ impl DedicatedPublicKeyChoice {
}
/// Sets the keyswitch constraints scheme constraints
pub fn with_pke_switch(mut self, pke_switch: PkeSwitchChoice) -> Self {
self.pke_switch = pke_switch;
pub fn with_pke_switch(mut self, pke_switch: PkeKeyswitchTargetChoice) -> Self {
self.pke_keyswitch_target = pke_switch;
self
}
@@ -443,7 +428,7 @@ impl DedicatedPublicKeyChoice {
return false;
}
self.pke_switch
self.pke_keyswitch_target
.is_compatible(dedicated_pk_params.ksk_params.destination_key)
&& self
.zk_scheme
@@ -455,7 +440,7 @@ impl Default for DedicatedPublicKeyChoice {
fn default() -> Self {
Self {
zk_scheme: CompactPkeZkSchemeChoice::allow_all(),
pke_switch: PkeSwitchChoice::allow_all(),
pke_keyswitch_target: PkeKeyswitchTargetChoice::allow_all(),
require_re_rand: false,
}
}
@@ -571,7 +556,7 @@ impl MetaParametersFinder {
self
}
/// Sets the block moduluses (MessageModulus, CarryModulus)
/// Sets the block modulus
///
/// Only MessageModulus is required as MessageModulus == CarryModulus is forced
pub const fn with_block_modulus(mut self, message_modulus: MessageModulus) -> Self {
@@ -703,8 +688,9 @@ impl MetaParametersFinder {
/// If more than one parameter is found, this function will apply its
/// own set of rule to select one of them:
/// * The parameter with highest pfail is prioritized
/// * CPU will favor classical PBS
/// * GPU will favor multi-bit PBS
/// * CPU will favor classical PBS, with TUniform
/// * GPU will favor multi-bit PBS, with TUniform
/// * ZKV2 is prioritized
pub fn find(&self) -> Option<MetaParameters> {
let mut candidates = self.named_find_all();
@@ -716,43 +702,64 @@ impl MetaParametersFinder {
return candidates.pop().map(|(param, _)| param);
}
fn filter_candidates(
candidates: Vec<(MetaParameters, &str)>,
filter: impl Fn(&(MetaParameters, &str)) -> bool,
) -> Vec<(MetaParameters, &str)> {
let filtered = candidates
.iter()
.copied()
.filter(filter)
.collect::<Vec<_>>();
// The filter keeps elements, based on what tfhe-rs sees as better default
// However, it's possible, that the input candidates list does not include
// anything that matches the filter
// e.g. the use selected MultiBitPBS on CPU, but our cpu filter prefers ClassicPBS
// therefor nothing will match, so we return the original list instead
if filtered.is_empty() {
candidates
} else {
filtered
}
}
let candidates = filter_candidates(candidates, |(params, _)| {
if let Some(pke_params) = params.dedicated_compact_public_key_parameters {
return pke_params.pke_params.zk_scheme == SupportedCompactPkeZkScheme::V2;
}
true
});
let mut candidates = match self.backend {
// On CPU we prefer Classical PBS with TUniform
Backend::Cpu => filter_candidates(candidates, |(params, _)| {
matches!(
params.compute_parameters,
AtomicPatternParameters::Standard(PBSParameters::PBS(_))
) && params.noise_distribution_kind() == NoiseDistributionKind::TUniform
}),
// On GPU we prefer MultiBit PBS with TUniform
Backend::CudaGpu => filter_candidates(candidates, |(params, _)| {
matches!(
params.compute_parameters,
AtomicPatternParameters::Standard(PBSParameters::MultiBitPBS(_))
) && params.noise_distribution_kind() == NoiseDistributionKind::TUniform
}),
};
// highest failure probability is the last element,
// and higher failure probability means better performance
//
// Since pfails are negative e.g: -128, -40 for 2^-128 and 2^-40
// the closest pfail the constraint is the last one
// the closest pfail to the constraint is the last one
candidates.sort_by(|(a, _), (b, _)| {
a.failure_probability()
.partial_cmp(&b.failure_probability())
.unwrap()
});
match self.backend {
// On CPU we prefer Classical PBS with TUniform
Backend::Cpu => candidates
.iter()
.rfind(|(params, _)| {
matches!(
params.compute_parameters,
AtomicPatternParameters::Standard(PBSParameters::PBS(_))
) && params.noise_distribution_kind() == NoiseDistributionKind::TUniform
})
.copied()
.or_else(|| candidates.pop())
.map(|(param, _)| param),
// On GPU we prefer MultiBit PBS with TUniform
Backend::CudaGpu => candidates
.iter()
.rfind(|(params, _)| {
matches!(
params.compute_parameters,
AtomicPatternParameters::Standard(PBSParameters::MultiBitPBS(_))
) && params.noise_distribution_kind() == NoiseDistributionKind::TUniform
})
.copied()
.or_else(|| candidates.pop())
.map(|(param, _)| param),
}
candidates.last().copied().map(|(params, _)| params)
}
/// Returns all known meta parameter that satisfy the choices
@@ -864,13 +871,15 @@ mod tests {
.with_dedicated_compact_public_key(Some(
DedicatedPublicKeyChoice::new()
.with_zk_scheme(CompactPkeZkSchemeChoice::not_used())
.with_pke_switch(PkeSwitchChoice::new().allow(EncryptionKeyChoice::Big)),
.with_pke_switch(
PkeKeyswitchTargetChoice::new().allow(EncryptionKeyChoice::Big),
),
));
let params = finder.find();
let mut expected =
super::super::v1_4::meta::cpu::V1_4_META_PARAM_CPU_2_2_KS_PBS_PKE_TO_BIG_ZKV1_TUNIFORM_2M128;
super::super::v1_4::meta::cpu::V1_4_META_PARAM_CPU_2_2_KS_PBS_PKE_TO_BIG_ZKV2_TUNIFORM_2M128;
expected.compression_parameters = None;
expected.noise_squashing_parameters = None;
expected
@@ -889,14 +898,16 @@ mod tests {
.with_dedicated_compact_public_key(Some(
DedicatedPublicKeyChoice::new()
.with_zk_scheme(CompactPkeZkSchemeChoice::not_used())
.with_pke_switch(PkeSwitchChoice::new().allow(EncryptionKeyChoice::Small))
.with_pke_switch(
PkeKeyswitchTargetChoice::new().allow(EncryptionKeyChoice::Small),
)
.with_re_randomization(true),
));
let params = finder.find();
let mut expected =
super::super::v1_4::meta::cpu::V1_4_META_PARAM_CPU_2_2_KS_PBS_PKE_TO_SMALL_ZKV1_TUNIFORM_2M128;
super::super::v1_4::meta::cpu::V1_4_META_PARAM_CPU_2_2_KS_PBS_PKE_TO_SMALL_ZKV2_TUNIFORM_2M128;
expected.compression_parameters = None;
expected.noise_squashing_parameters = None;
assert_eq!(params, Some(expected));

View File

@@ -79,7 +79,11 @@ pub use compact_public_key_only::{
pub use coverage_parameters::*;
pub use key_switching::ShortintKeySwitchingParameters;
pub use ks32::KeySwitch32PBSParameters;
pub use meta::MetaParameters;
pub use meta::{
AtomicPatternChoice, CompactPkeZkSchemeChoice, Constraint, DedicatedPublicKeyChoice, Log2PFail,
MetaParameters, MetaParametersFinder, MultiBitPBSChoice, NoiseDistributionChoice,
NoiseDistributionKind, NoiseSquashingChoice, PkeKeyswitchTargetChoice, Version,
};
pub use multi_bit::MultiBitPBSParameters;
pub use noise_squashing::{
MetaNoiseSquashingParameters, NoiseSquashingClassicParameters,