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

@@ -5,6 +5,7 @@ This document explains how the choice of cryptographic parameters impacts both t
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,6 +49,7 @@ 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.
@@ -60,7 +61,7 @@ The following example shows how to compress and decompress a list containing 4 m
```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,