Memory out-of-bounds when converting to HE domain

Hello, I am trying to convert a project to HE domain, and had the following issue when I was using Palisade (now I plan to use OpenFHE instead and I think that the memory consumed will reduce, but haven’t tested this out) -

I noticed that for creation of one ciphertext, my program takes ~43MB RAM, and for ‘n’ ciphertexts in general(large n), takes ‘n’ MB RAM.
In my current benchmark, I am creating an array[10^8], and want to create ciphertext out of each element in the array, which is shooting up the RAM needed.

I am pasting a piece of the Palisade code I am using, where multiple ciphertexts are being created, and using this, if you create something like vecCT v(1000000000), it causes memory overflow.

Can you please tell if Palisade indeed takes lot of memory for ciphertext creation, and if there is any workaround to reduce memory usage by program in case lots of ciphertexts are created?

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

#include "palisade.h"

using namespace lbcrypto;
using namespace std;

extern uint64_t p; // = 65537;
extern double sigma; //= 3.2; 
extern SecurityLevel securityLevel ; //= HEStd_128_classic;
extern uint32_t depth; // = 2;
extern CryptoContext<DCRTPoly> cc; // = CryptoContextFactory<DCRTPoly>::genCryptoContextBFVrns(p, securityLevel, sigma, 0, depth, 0, OPTIMIZED);
extern int32_t n; // = cc->GetCryptoParameters()->GetElementParams()->GetCyclotomicOrder()/2; 
extern LPKeyPair<DCRTPoly> kp; 
extern bool init_flag;

//data types we will need
using CT = Ciphertext<DCRTPoly> ; //ciphertext
using PT = Plaintext ; //plaintext
using vecCT = vector<CT>; //vector of ciphertexts
using vecPT = vector<PT>; //vector of plaintexts
using vecInt = vector<int64_t>; // vector of ints
using vecChar = vector<char>; // vector of characters

// uint64_t p = 65537;
uint64_t p=12869861377;
double sigma = 3.2; 
SecurityLevel securityLevel = HEStd_128_classic;
uint32_t depth = 3;                                   
CryptoContext<DCRTPoly> cc = CryptoContextFactory<DCRTPoly>::genCryptoContextBFVrns(p, securityLevel, sigma, 0, depth, 0, OPTIMIZED);
int32_t n = cc->GetCryptoParameters()->GetElementParams()->GetCyclotomicOrder()/2; 
LPKeyPair<DCRTPoly> kp;
bool init_flag=false;

void init()   
        // cc->EvalAtIndexKeyGen(kp.secretKey, {1, 2, -1, -2});

vector<int64_t> decrypt_ciphertext_to_plaintext_vector(Ciphertext<DCRTPoly> ciphertext)   
    Plaintext plaintext;                                       
    cc->Decrypt(kp.secretKey, ciphertext, &plaintext);         
    vector<int64_t> v=plaintext->GetPackedValue();       
    return v;                                                  

/* Put integer as single element in a plaintext vector, then encrypt that vector and return the ciphertext */
Ciphertext<DCRTPoly> encrypt_plaintext_integer_to_ciphertext(int64_t d)   
    int64_t c=d;
    vector<int64_t> vec;                        
    Plaintext plaintext = cc->MakePackedPlaintext(vec);
    auto ciphertext = cc->Encrypt(kp.publicKey, plaintext); 
    return ciphertext;                                      

int main() {
  CT c=encrypt_plaintext_integer_to_ciphertext(10); CT c2=encrypt_plaintext_integer_to_ciphertext(1); 
  CT c3=encrypt_plaintext_integer_to_ciphertext(10); CT c4=encrypt_plaintext_integer_to_ciphertext(1); 
  CT c5=encrypt_plaintext_integer_to_ciphertext(10); CT c6=encrypt_plaintext_integer_to_ciphertext(1); 
  CT c7=encrypt_plaintext_integer_to_ciphertext(10); CT c8=encrypt_plaintext_integer_to_ciphertext(1); 
  CT c9=encrypt_plaintext_integer_to_ciphertext(10); CT c10=encrypt_plaintext_integer_to_ciphertext(1); 
  CT c11=encrypt_plaintext_integer_to_ciphertext(10); CT c12=encrypt_plaintext_integer_to_ciphertext(1); 
  CT c13=encrypt_plaintext_integer_to_ciphertext(10); CT c14=encrypt_plaintext_integer_to_ciphertext(1); 
  CT c15=encrypt_plaintext_integer_to_ciphertext(10); CT c16=encrypt_plaintext_integer_to_ciphertext(1); 
  CT c17=encrypt_plaintext_integer_to_ciphertext(10); CT c18=encrypt_plaintext_integer_to_ciphertext(1); 
  CT c19=encrypt_plaintext_integer_to_ciphertext(10); CT c20=encrypt_plaintext_integer_to_ciphertext(1); 

// High memory needed for below 
// vecCT vv(1000000000);                         
// for(int i=0;i<1000000000;i++)
// {
//    vv[i]=encrypt_plaintext_integer_to_ciphertext(i);
//    vv[i]=cc->EvalAdd(vv[i],encrypt_plaintext_integer_to_ciphertext(1);
// }

  return 0;

Do you still have the same issue in OpenFHE?

I was actually not able to build my project yet using OpenFHE, because I would have to write a custom cmake file for creating the object files of my project and then linking them. I will post any updates.

I observed that when I was running a simple Palisade program creating 1000 BFV ciphertexts, program memory consumed was 1 GB, whereas when OpenFHE program using 1000 ciphertexts was written, program memory was 1 MB. Was this just for me for some reason or is this right and there is some explanation for this? If Palisade and OpenFHE are similar in code, there should be no difference in program memory usage right?

Check the cryptographic parameters: ring dimension and multiplicative depth. These are the main parameters that drive the ciphertext size.

1 Like

Bump to follow up on this

The current parameters of my program are-

    CCParams<CryptoContextBFVRNS> parameters;
    // parameters.SetRingDim(100000);
    // parameters.SetSecurityLevel(HEStd_128_classic);

    CryptoContext<DCRTPoly> cryptoContext = GenCryptoContext(parameters);
    // Enable features that you wish to use

Using this, creating 1000 ciphertexts is taking upto 400MB.

And apologies for me saying earlier that OpenFHE takes lesser memory than Palisade- I was running OpenFHE with HEStd_NotSet.
Now I check that both Palisade and OpenFHE take same memory.

How to reduce the memory usage now (how to set optimal ring dimension and multiplicative depth)?

Check how many successive multiplications a long one path the computation of interest needs and set the multiplicative depth accordingly using parameters.SetMultiplicativeDepth(depth).
To understand the concept of multiplicative depth, here are some examples:

Example: \mathbf{a}, \mathbf{b}, \mathbf{c}, \mathbf{d} are all encrypted vectors.

  1. Homomorphic evaluation of \mathbf{a}\cdot \mathbf{b} + \mathbf{c}\cdot \mathbf{d} is of multiplicative depth = 1
  2. However, computing \mathbf{a}\cdot \mathbf{b} \cdot \mathbf{c}\cdot \mathbf{d} is at least of multiplicative depth = 2, (\mathbf{a}\cdot \mathbf{b}) \cdot (\mathbf{c}\cdot \mathbf{d}), the quantities in parentheses can be evaluated in parallel with depth 1.

Once you optimize the multiplicative depth, OpenFHE will choose the minimum ring size to support the desired security level. You can always reduce the ring dimension and ignore the recommended security level, but that is not recommended.

Try to think of a method to stream your computation if possible by generating a subset of all ciphertexts, processing them, and repeating for the rest.