Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions benchmark/src/ckks-functional-bootstrapping.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,10 @@ struct fbt_config {
while (state.KeepRunning()) {
if (binaryLUT)
cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0,
0, t.order);
0, t.order, t.numSlots);
else
cc->EvalFBTSetup(coeffcomp, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0,
0, t.order);
0, t.order, t.numSlots);
}

cc->ClearStaticMapsAndVectors();
Expand Down Expand Up @@ -242,10 +242,10 @@ struct fbt_config {

if (binaryLUT)
cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, 0,
t.order);
t.order, t.numSlots);
else
cc->EvalFBTSetup(coeffcomp, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, 0,
t.order);
t.order, t.numSlots);

for (auto _ : state) {
state.PauseTiming();
Expand Down Expand Up @@ -315,10 +315,10 @@ struct fbt_config {

if (binaryLUT)
cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, 0,
t.order);
t.order, t.numSlots);
else
cc->EvalFBTSetup(coeffcomp, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, 0,
t.order);
t.order, t.numSlots);

cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS);
cc->EvalMultKeyGen(keyPair.secretKey);
Expand Down Expand Up @@ -413,10 +413,10 @@ struct fbt_config {

if (binaryLUT)
cc->EvalFBTSetup(coeffintMod, numSlotsCKKS, t.POutput, t.PInput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0,
0, t.order);
0, t.order, t.numSlots);
else
cc->EvalFBTSetup(coeffcompMod, numSlotsCKKS, t.POutput, t.PInput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0,
0, t.order);
0, t.order, t.numSlots);

cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS);
cc->EvalMultKeyGen(keyPair.secretKey);
Expand Down
31 changes: 23 additions & 8 deletions src/pke/examples/CKKS_FUNCTIONAL_BOOTSTRAPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,19 @@ The RLWE and CKKS schemes are compatible in the sense that they can share the sa
makes the conversion between RLWE and CKKS to essentially be modulus switching.

A vector encoded in RLWE can have as many elements as the ring dimension. If this is the case (the number of elements is between half
the ring dimension and the ring dimension), this corresponds to a full complex packing in CKKS, where the complex slots are also
populated. Otherwise, real CKKS packing (full or sparse) is used.
the ring dimension and the ring dimension), this corresponds to a full complex packing in CKKS, where the imaginary parts of the slots are also
populated, and functional bootstrapping processes two ciphertexts (the real and imaginary channels of the homomorphic encoding).
Otherwise, real CKKS packing (full or sparse) is used, and functional bootstrapping processes a single ciphertext. The number of
elements is passed to `EvalFBTSetup` as `numValues`, which selects the layout independently of the CKKS data type; it defaults to
as many elements as the slots can hold (the ring dimension for full packing). In particular, for a number of elements equal to half
the ring dimension, the values occupy the real parts of the N/2 slots, and passing `numValues` = N/2 halves the cost of the
approximate modular reduction and of the look-up table evaluation compared to the full complex packing.

Functional bootstrapping itself does not need a particular CKKS data type. With full complex packing, however, the imaginary
parts of the slots carry half of the values, so the CKKS data type should be set to COMPLEX (`SetCKKSDataType(COMPLEX)`) if
complex plaintexts are encoded or the CKKS ciphertext is decrypted before `EvalHomDecoding`, e.g., in a leveled computation
between `EvalFBTNoDecoding` and `EvalHomDecoding`: with the default REAL data type, encoding zeroes the imaginary parts of a
plaintext and decryption treats the imaginary parts of the slots as noise.

Note that the message is encoded in the coefficients in the RLWE scheme, but when converted to CKKS, the coefficients are held in
the slots of the CKKS ciphertexts. To be able to apply CKKS leveled computations, we need to homomorphically transform from
Expand Down Expand Up @@ -113,6 +124,7 @@ The SPARSE_TERNARY distribution is the distribution used in the original CKKS pa
where the Hamming weight of the secret key is set to 192. For the set number of overflows in bootstrapping, K = 28, this
distribution leads to a larger probability of failure (about 2^-22 for 2^16 slots), so it should not be used in deployments.
Compared to SPARSE_ENCAPSULATED, choosing this distribution requires an extra level in the complex exponential approximation
(a degree-69 Chebyshev interpolation over [-28, 28] instead of degree 46 over [-16, 16], used for all supported LUT sizes)
to achieve correctness. This is the distribution used for the benchmarks in [[AKP25]](https://eprint.iacr.org/2024/1623.pdf).

The SPARSE_ENCAPSULATED distribution (described in [[BTH22]](https://eprint.iacr.org/2022/024.pdf)) uses a Hamming weight of 32
Expand All @@ -134,10 +146,10 @@ used when uniform ternary secrets are required for compliance with them. It is h
bootstrapping: the number of overflows is bounded by K = 672 (probability of failure below 2^-73
for N = 2^16 and 2^-30 for N = 2^17 with full packing), and the complex exponential (or cosine, for the binary case) is
approximated by a degree-104 Chebyshev interpolation over [-672, 672] followed by six double-angle iterations (instead of degree
64/46 and two double-angle iterations for the sparse distributions). This increases the
multiplicative depth of functional bootstrapping by 5 levels. In addition, since the mod-raised message is scaled down by K
before the homomorphic encoding, larger scaling factors (roughly 5-9 more bits, depending on the parameters) are needed to
achieve the same output noise as for the sparse distributions.
69/46 and two double-angle iterations for the sparse distributions). This increases the multiplicative depth of functional
bootstrapping by 5 levels compared to SPARSE_ENCAPSULATED (4 compared to SPARSE_TERNARY). In addition, since the mod-raised
message is scaled down by K before the homomorphic encoding, larger scaling factors (roughly 5-9 more bits, depending on the
parameters) are needed to achieve the same output noise as for the sparse distributions.

**Scaling techniques**
All scaling techniques (FIXEDMANUAL, FIXEDAUTO, FLEXIBLEAUTO, FLEXIBLEAUTOEXT, COMPOSITESCALINGAUTO, and
Expand Down Expand Up @@ -190,8 +202,11 @@ coefficient vector c = (c_0, c_1, ..., c_d) has to satisfy
$$g(m) \approx 2 \cdot \mathrm{Re}\left(\sum_{j=0}^{d} c_j e^{\pi i j m}\right), \quad m \in [-1/2, 1/2).$$

In other words, c_0 is half of the constant term of the series and c_j, for j > 0, are the one-sided coefficients of the
series (the conjugate terms are added internally). The output is always real-valued; with CKKSDataType COMPLEX, the imaginary
parts of the input slots are discarded, so CKKSDataType REAL should be used unless complex values are needed elsewhere in the
series (the conjugate terms are added internally). The function is applied slot-wise. With CKKSDataType REAL, it is applied to
the (real) slot values and a single ciphertext goes through the complex exponential and the series. With CKKSDataType COMPLEX, it
is applied to the real and imaginary parts separately (a slot a + ib yields f(a) + i f(b)): the two parts are processed as two
ciphertexts, as in the slots-encoding CKKS bootstrapping and in the complex full packing of the discrete functional bootstrapping,
so CKKSDataType REAL should be used unless complex values are needed elsewhere in the
computation.

The coefficients are computed offline with the Python scripts in [fefbt-python](https://github.com/openfheorg/fefbt-python),
Expand Down
3 changes: 2 additions & 1 deletion src/pke/examples/FE-functional-bootstrapping-ckks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ int main() {
levelsAfterBootstrapping;

CCParams<CryptoContextCKKSRNS> parameters;
parameters.SetCKKSDataType(COMPLEX);
// the input is real-valued; with COMPLEX the function would also be applied to the (zero) imaginary parts
parameters.SetCKKSDataType(REAL);
parameters.SetSecretKeyDist(skd);
parameters.SetSecurityLevel(HEStd_NotSet);
parameters.SetRingDim(ringDim);
Expand Down
45 changes: 31 additions & 14 deletions src/pke/examples/functional-bootstrapping-ckks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,28 @@ int main() {
std::cerr << "=====Boolean LUT order 2 sparsely packed=====" << std::endl << std::endl;
ArbitraryLUT(QBFVINIT, BigInteger(2), BigInteger(2), (BigInteger(1) << 33), (BigInteger(1) << 33), 1, 2, 8, 4096,
[](int64_t x) { return (x % 2 - 2 / 2) % 2; });
std::cerr << "=====Boolean LUT order 1 fully packed=====" << std::endl << std::endl;
ArbitraryLUT(QBFVINIT, BigInteger(2), BigInteger(2), (BigInteger(1) << 33), (BigInteger(1) << 33), 1, 1, 1024, 4096,
// N values: complex full packing, two ciphertexts go through the modular reduction
std::cerr << "=====Boolean LUT order 1 fully packed (N values)=====" << std::endl << std::endl;
ArbitraryLUT(QBFVINIT, BigInteger(2), BigInteger(2), (BigInteger(1) << 33), (BigInteger(1) << 33), 1, 1, 4096, 4096,
[](int64_t x) { return (x % 2 - 2 / 2) % 2; });
// N/2 values: real full packing, a single ciphertext goes through the modular reduction
std::cerr << "=====Boolean LUT order 1 fully packed (N/2 values)=====" << std::endl << std::endl;
ArbitraryLUT(QBFVINIT, BigInteger(2), BigInteger(2), (BigInteger(1) << 33), (BigInteger(1) << 33), 1, 1, 2048, 4096,
[](int64_t x) { return (x % 2 - 2 / 2) % 2; });
// LUT with 8-bit input and 4-bit output
std::cerr << "=====8-to-4 bit LUT order 1 sparsely packed=====" << std::endl << std::endl;
ArbitraryLUT(QBFVINIT, BigInteger(256), BigInteger(16), (BigInteger(1) << 47), (BigInteger(1) << 47), 32, 1, 8,
4096, [](int64_t x) { return (x % 256 - 16 / 2) % 16; });
std::cerr << "=====8-to-4 bit LUT order 1 fully packed (N values)=====" << std::endl << std::endl;
ArbitraryLUT(QBFVINIT, BigInteger(256), BigInteger(16), (BigInteger(1) << 47), (BigInteger(1) << 47), 32, 1, 4096,
4096, [](int64_t x) { return (x % 256 - 16 / 2) % 16; });
std::cerr << "=====8-to-4 bit LUT order 1 fully packed (N/2 values)=====" << std::endl << std::endl;
ArbitraryLUT(QBFVINIT, BigInteger(256), BigInteger(16), (BigInteger(1) << 47), (BigInteger(1) << 47), 32, 1, 2048,
4096, [](int64_t x) { return (x % 256 - 16 / 2) % 16; });

std::cerr << "\n\n*2.* Compute multiple functions over the same ciphertext." << std::endl << std::endl;
// Two LUTs with 8-bit input and 8-bit output and intermediate leveled computations
std::cerr << "=====Multivalue bootstrapping for two 8-to-8 bit LUTs order 1 fully packed=====" << std::endl
std::cerr << "=====Multivalue bootstrapping for two 8-to-8 bit LUTs order 1 sparsely packed=====" << std::endl
<< std::endl;
MultiValueBootstrapping(QBFVINIT, BigInteger(256), BigInteger(256), (BigInteger(1) << 47), (BigInteger(1) << 47),
32, 1, 256, 2048, 1);
Expand All @@ -90,11 +101,11 @@ int main() {
<< std::endl;
MultiPrecisionSign(QBFVINIT, BigInteger(4096), BigInteger(2), (BigInteger(1) << 46), (BigInteger(1) << 35), 1, 1, 1,
32, 2048);
std::cerr << "=====Sign evaluation of a 12-bit input using 4-bit digits order 1 fully packed=====" << std::endl
std::cerr << "=====Sign evaluation of a 12-bit input using 4-bit digits order 1 sparsely packed=====" << std::endl
<< std::endl;
MultiPrecisionSign(QBFVINIT, BigInteger(4096), BigInteger(16), (BigInteger(1) << 48), (BigInteger(1) << 40), 32, 8,
1, 64, 2048);
std::cerr << "=====Sign evaluation of a 32-bit input using 8-bit digits order 1 fully packed=====" << std::endl
std::cerr << "=====Sign evaluation of a 32-bit input using 8-bit digits order 1 sparsely packed=====" << std::endl
<< std::endl;
MultiPrecisionSign(QBFVINITLARGE, BigInteger(1) << 32, BigInteger(256), BigInteger(1) << 71, BigInteger(1) << 47,
256, 32, 1, 64, 2048);
Expand All @@ -107,7 +118,9 @@ void ArbitraryLUT(BigInteger QBFVInit, BigInteger PInput, BigInteger POutput, Bi
std::function<int64_t(int64_t)> func) {
/* 1. Figure out whether sparse packing or full packing should be used.
* numSlots represents the number of values to be encrypted in BFV.
* If this number is the same as the ring dimension, then the CKKS slots is half.
* If this number is the same as the ring dimension, then the CKKS slots is half (the imaginary parts of
* the slots carry the second half of the values). EvalFBTSetup takes the number of values to select the
* layout: for N/2 values, a single ciphertext goes through the modular reduction.
*/
bool flagSP = (numSlots <= ringDim / 2); // sparse packing
auto numSlotsCKKS = flagSP ? numSlots : numSlots / 2;
Expand Down Expand Up @@ -198,10 +211,10 @@ void ArbitraryLUT(BigInteger QBFVInit, BigInteger PInput, BigInteger POutput, Bi

if (binaryLUT)
cc->EvalFBTSetup(coeffint, numSlotsCKKS, PInput, POutput, Bigq, keyPair.publicKey, {0, 0}, lvlb,
levelsAvailableAfterBootstrap, 0, order);
levelsAvailableAfterBootstrap, 0, order, numSlots);
else
cc->EvalFBTSetup(coeffcomp, numSlotsCKKS, PInput, POutput, Bigq, keyPair.publicKey, {0, 0}, lvlb,
levelsAvailableAfterBootstrap, 0, order);
levelsAvailableAfterBootstrap, 0, order, numSlots);

cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS);
cc->EvalMultKeyGen(keyPair.secretKey);
Expand Down Expand Up @@ -255,7 +268,9 @@ void MultiValueBootstrapping(BigInteger QBFVInit, BigInteger PInput, BigInteger
uint32_t levelsComputation) {
/* 1. Figure out whether sparse packing or full packing should be used.
* numSlots represents the number of values to be encrypted in BFV.
* If this number is the same as the ring dimension, then the CKKS slots is half.
* If this number is the same as the ring dimension, then the CKKS slots is half (the imaginary parts of
* the slots carry the second half of the values). EvalFBTSetup takes the number of values to select the
* layout: for N/2 values, a single ciphertext goes through the modular reduction.
*/
bool flagSP = (numSlots <= ringDim / 2); // sparse packing
auto numSlotsCKKS = flagSP ? numSlots : numSlots / 2;
Expand Down Expand Up @@ -360,10 +375,10 @@ void MultiValueBootstrapping(BigInteger QBFVInit, BigInteger PInput, BigInteger

if (binaryLUT)
cc->EvalFBTSetup(coeffint1, numSlotsCKKS, PInput, POutput, Bigq, keyPair.publicKey, {0, 0}, lvlb,
levelsAvailableAfterBootstrap, levelsComputation, order);
levelsAvailableAfterBootstrap, levelsComputation, order, numSlots);
else
cc->EvalFBTSetup(coeffcomp1, numSlotsCKKS, PInput, POutput, Bigq, keyPair.publicKey, {0, 0}, lvlb,
levelsAvailableAfterBootstrap, levelsComputation, order);
levelsAvailableAfterBootstrap, levelsComputation, order, numSlots);

cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS);
cc->EvalMultKeyGen(keyPair.secretKey);
Expand Down Expand Up @@ -499,7 +514,9 @@ void MultiPrecisionSign(BigInteger QBFVInit, BigInteger PInput, BigInteger PDigi
uint64_t scaleTHI, uint64_t scaleStepTHI, size_t order, uint32_t numSlots, uint32_t ringDim) {
/* 1. Figure out whether sparse packing or full packing should be used.
* numSlots represents the number of values to be encrypted in BFV.
* If this number is the same as the ring dimension, then the CKKS slots is half.
* If this number is the same as the ring dimension, then the CKKS slots is half (the imaginary parts of
* the slots carry the second half of the values). EvalFBTSetup takes the number of values to select the
* layout: for N/2 values, a single ciphertext goes through the modular reduction.
*/
bool flagSP = (numSlots <= ringDim / 2); // sparse packing
auto numSlotsCKKS = flagSP ? numSlots : numSlots / 2;
Expand Down Expand Up @@ -614,10 +631,10 @@ void MultiPrecisionSign(BigInteger QBFVInit, BigInteger PInput, BigInteger PDigi

if (binaryLUT)
cc->EvalFBTSetup(coeffintMod, numSlotsCKKS, PDigit, PInput, Bigq, keyPair.publicKey, {0, 0}, lvlb,
levelsAvailableAfterBootstrap, 0, order);
levelsAvailableAfterBootstrap, 0, order, numSlots);
else
cc->EvalFBTSetup(coeffcompMod, numSlotsCKKS, PDigit, PInput, Bigq, keyPair.publicKey, {0, 0}, lvlb,
levelsAvailableAfterBootstrap, 0, order);
levelsAvailableAfterBootstrap, 0, order, numSlots);

cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS);

Expand Down
Loading