About precision, scale value and basic operation of boot strapping

While using bootstrap with the following program, three questions came to mind.

  1. Isn’t the accuracy of the bootstrapped ciphertext low?

I can observe a 50-bit → 10-bit error.

  1. The scale value of the bootstrapped ciphertext is squared. It seems like (scale) is correct, not (scale^2).

  2. As a post-bootstrap calculation practice, I multiplied 1 by the bootstrapped ciphertext and got the wrong answer.

The program is as follows.

#define PROFILE

#include "openfhe.h"

using namespace lbcrypto;

int main(int argc, char* argv[]) {
    CCParams<CryptoContextCKKSRNS> parameters;

    SecretKeyDist secretKeyDist = SPARSE_TERNARY;
    parameters.SetSecretKeyDist(secretKeyDist);

    parameters.SetSecurityLevel(HEStd_128_classic);
    parameters.SetRingDim(1 << 16);

    parameters.SetNumLargeDigits(3);
    parameters.SetKeySwitchTechnique(HYBRID);

    ScalingTechnique rescaleTech = FIXEDMANUAL;
    usint dcrtBits               = 50;
    usint firstMod               = 59;

    parameters.SetScalingModSize(dcrtBits);
    parameters.SetScalingTechnique(rescaleTech);
    parameters.SetFirstModSize(firstMod);

    std::vector<uint32_t> levelBudget = {3, 3};

    std::vector<uint32_t> bsgsDim = {0, 0};

    uint32_t approxBootstrapDepth = 9;

    uint32_t levelsAvailableAfterBootstrap = 6;
    usint depth = levelsAvailableAfterBootstrap + FHECKKSRNS::GetBootstrapDepth(approxBootstrapDepth, levelBudget, secretKeyDist);
    parameters.SetMultiplicativeDepth(depth);

    CryptoContext<DCRTPoly> cryptoContext = GenCryptoContext(parameters);

    cryptoContext->Enable(PKE);
    cryptoContext->Enable(KEYSWITCH);
    cryptoContext->Enable(LEVELEDSHE);
    cryptoContext->Enable(ADVANCEDSHE);
    cryptoContext->Enable(FHE);

    uint32_t numSlots=8;

    usint ringDim = cryptoContext->GetRingDimension();
    std::cout << "CKKS scheme is using ring dimension " << ringDim << std::endl << std::endl;

    cryptoContext->EvalBootstrapSetup(levelBudget, bsgsDim, numSlots);

    auto keyPair = cryptoContext->KeyGen();
    cryptoContext->EvalMultKeyGen(keyPair.secretKey);
    
    cryptoContext->EvalBootstrapKeyGen(keyPair.secretKey, numSlots);

    std::vector<double> x;
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<> dis(0.0, 1.0);
    for (size_t i = 0; i < numSlots; i++) {
        x.push_back(dis(gen));
    }

    Plaintext ptxt = cryptoContext->MakeCKKSPackedPlaintext(x, 1, depth - 1, nullptr, numSlots);
    ptxt->SetLength(numSlots);
    std::cout << "Input: " << ptxt << std::endl;

    Ciphertext<DCRTPoly> ciph = cryptoContext->Encrypt(keyPair.publicKey, ptxt);

    std::cout << "Initial number of levels remaining: " << depth - ciph->GetLevel() << std::endl;

    auto ciphertextAfter = cryptoContext->EvalBootstrap(ciph);

    std::cout << "Number of levels remaining after bootstrapping: " << depth - ciphertextAfter->GetLevel() << std::endl
              << std::endl;

    std::cout << "ciphertextAfter scale value : " << ciphertextAfter->GetScalingFactor() << std::endl << std::endl;

    ciphertextAfter = cryptoContext->Rescale(ciphertextAfter);

    Plaintext result;
    cryptoContext->Decrypt(keyPair.secretKey, ciphertextAfter, &result);
    result->SetLength(numSlots);
    std::cout << "Output after bootstrapping \n\t" << result << std::endl;

    std::vector<double> example(8, 1);
    Plaintext exmaple_p = cryptoContext->MakeCKKSPackedPlaintext(example);

    Ciphertext<DCRTPoly> exmaple_result = cryptoContext->EvalMult(ciphertextAfter, ptxt);

    exmaple_result = cryptoContext->Rescale(exmaple_result);

    cryptoContext->Decrypt(keyPair.secretKey, exmaple_result, &result);
    result->SetLength(numSlots);
    std::cout << "Output of multiplying \n\t" << result << std::endl;
}

The output is as follows.

CKKS scheme is using ring dimension 65536

Input: (0.62200457, 0.91855678, 0.47443048, 0.13907816, 0.75322639, 0.31923514, 0.44027182, 0.6205233, ... ); Estimated precision: 50 bits

Initial number of levels remaining: 1
Number of levels remaining after bootstrapping: 6

ciphertextAfter scale value : 1.26765e+30

Output after bootstrapping 
        (0.62120326, 0.91950136, 0.47396747, 0.13998224, 0.75340914, 0.31880892, 0.44032047, 0.62021078, ... ); Estimated precision: 10 bits

Output of multiplying 
        (0.38636131, 0.84450783, 0.22584997, 0.019939034, 0.5680056, 0.10228517, 0.19387869, 0.38522217, ... ); Estimated precision: 11 bits

Please let me know if I’m wrong or have any comments regarding these questions.

Thank you in advance.

FIXEDMANUAL and FIXEDAUTO modes are known to have a precision loss compared to the flexible modes. If you use FLEXIBLEAUTOEXT you will see a much better precision. Take a look here and at the advanced-real-numbers example in OpenFHE.

Thank you for your reply.

As you said, when I changed from FIXEDMANUAL to FLEXIBLEAUTOEXT, the bootstrapping accuracy improved by 4 bits.

This solves my first question.

Thank you very much.

Regarding my second question, is the fact that the scale value of the ciphertext after bootstrapping is squared a specification of OpenFHE?

I also solved my third question myself.

After a multiplication, the scale will be \Delta^2. In order to reduce noise, the best place to perform a rescaling is not immediately after the multiplication, but right before the next multiplication (which is what the AUTO modes do automatically, without user intervention).

FIXEDMANUAL should be used by experts to tailor the locations of the rescaling/modulus reduction for speed. The modulus reduction is not done as a last step inside bootstrapping since usually bootstrapping is not the last operation, so there could be cases where it is better to do modulus reduction after some more operations over the bootstrapping result. It is the responsibility of a user who employs FIXEDMANUAL to track the scaling factor across all operations, as mentioned in the OpenFHE paper reference above; this can be done also with GetNoiseScaleDeg().