First of all, thank you for FIDESlib — the GPU performance is remarkable (we measured bootstrapping at ~10 ms and deg-31 Chebyshev evaluation at ~7 ms at N=2^16, versus seconds on CPU), and the OpenFHE-interoperable design made it straightforward to try against an existing workload. We hit the issue below while running a bootstrapped CKKS workload (an encrypted transformer inference) that was designed and validated against stock OpenFHE 1.5.1, and we hope the detailed repro makes it easy to pin down.
Summary
With SPARSE_ENCAPSULATED secret keys, EvalBootstrap on GPU completes but returns results unrelated to the input (max error ≈ 1.0 on inputs in [-1, 1]), and EvalBootstrap(ct, 2, 17) crashes (SIGSEGV inside an OpenMP parallel region). UNIFORM_TERNARY bootstrapping also shows ~2^-1 error. SPARSE_TERNARY is correct (2^-9.8) with the identical harness and parameters, which we believe rules out a problem on our side.
Notably, the patched OpenFHE that deps/build.sh installs handles SPARSE_ENCAPSULATED correctly when driven directly through lbcrypto (2^-10.9 single-iteration, 2^-28.0 with numIterations=2), so the issue appears to live in the FIDESlib layer rather than in the patched OpenFHE.
Tested at main @ 786c760 ("Fix issue #31").
Measured matrix
Parameters: N = 2^16, depth 26, ScalingModSize 50, FirstModSize 51, FLEXIBLEAUTO, NumLargeDigits 4, HEStd_128_classic, 32768 slots, EvalBootstrapSetup({3,3}, {0,0}, 32768), input linspace[-1, 1] burned to level 21 with plaintext multiplies so the bootstrap is real (not the fresh-ciphertext no-op).
| build |
keydist |
1-iter max err |
2-iter (prec 17) |
| stock OpenFHE 1.5.1 (CPU) |
SPARSE_ENCAPSULATED |
4.7e-4 (2^-11.0) |
5.1e-9 (2^-27.5) |
patched OpenFHE from deps/build.sh, pure lbcrypto (CPU) |
SPARSE_ENCAPSULATED |
5.1e-4 (2^-10.9) |
3.7e-9 (2^-28.0) |
| FIDESlib, GPU |
SPARSE_ENCAPSULATED |
~1.0 (output unrelated to input) |
SIGSEGV |
| FIDESlib, GPU |
SPARSE_TERNARY |
1.2e-3 (2^-9.8) ✓ |
1.2e-3 |
| FIDESlib, GPU |
UNIFORM_TERNARY |
5.0e-1 (2^-1.0) |
4.2e-1 |
Two smaller observations we can split into separate issues if you prefer:
- The CPU fallback of
EvalMult(const Ciphertext<DCRTPoly>&, Plaintext&) (api/CryptoContext.cpp:1220, taken when devices is empty) does std::any_cast<const lbcrypto::ConstPlaintext&>(pt->cpu), but MakeCKKSPackedPlaintext stores an lbcrypto::Plaintext; since std::any_cast requires an exact type match, this path always throws std::bad_any_cast.
- With
SPARSE_TERNARY (where bootstrapping works), numIterations = 2 does not reduce the error (2^-9.76 → 2^-9.72), where stock OpenFHE improves 2^-11 → 2^-27.5 — iterated bootstrapping may not be wired through.
Environment
- GPU: NVIDIA GeForce RTX 5090 (32 GB), driver 580.159.04
- CUDA 13.0 (V13.0.88), sm_120
- Rocky/RHEL 9.8, GCC 14.2.1 (gcc-toolset-14), CMake 3.31.8
- FIDESlib
main @ 786c760, built with -DFIDESLIB_INSTALL_OPENFHE=ON (its own patched OpenFHE), tests/benchmarks off
- Single device,
SetDevices({0})
Reproduce
One self-contained source file, built once against FIDESlib and once against the patched OpenFHE alone as the control. Build commands and the full expected-output matrix are in the README of the attached sample; the core is:
cmake -S . -B build-fl -DUSE_FIDESLIB=ON \
-Dfideslib_DIR=<prefix>/share/fideslib/cmake \
-DOpenFHE_DIR=<patched-openfhe-prefix>/lib/OpenFHE
cmake --build build-fl -j
./build-fl/boot_precision_repro # encapsulated: garbage, then crash
KEYDIST=sparse ./build-fl/boot_precision_repro # correct (control)
KEYDIST=uniform ./build-fl/boot_precision_repro # ~2^-1
boot_precision_repro.cpp (click to expand)
// Minimum reproducible sample: CKKS bootstrapping in FIDESlib returns
// garbage under SPARSE_ENCAPSULATED (max err ~1.0 one-iteration; crash
// with numIterations=2) and ~2^-1 under UNIFORM_TERNARY, while
// SPARSE_TERNARY is correct (2^-9.8) — and stock OpenFHE 1.5.1 handles
// SPARSE_ENCAPSULATED at 2^-11 / 2^-27.5 at identical parameters.
//
// Build twice from this one file (see CMakeLists.txt / README.md):
// baseline: links stock OpenFHE 1.5.1, namespace lbcrypto
// fideslib: -DUSE_FIDESLIB, links fideslib, namespace fideslib
//
// Optional env:
// KEYDIST = encapsulated (default) | sparse | uniform
//
// Expected output: see the measured matrix in README.md.
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
#ifdef USE_FIDESLIB
#include <fideslib.hpp>
using namespace fideslib;
#else
#include "openfhe.h"
using namespace lbcrypto;
#endif
static const uint32_t SLOTS = 32768;
int main() {
CCParams<CryptoContextCKKSRNS> p;
p.SetMultiplicativeDepth(26);
p.SetScalingModSize(50);
p.SetFirstModSize(51);
p.SetScalingTechnique(FLEXIBLEAUTO);
p.SetSecurityLevel(HEStd_128_classic);
p.SetRingDim(65536);
p.SetNumLargeDigits(4);
p.SetBatchSize(SLOTS);
const char* kd = std::getenv("KEYDIST");
std::string k = kd ? kd : "encapsulated";
p.SetSecretKeyDist(k == "sparse" ? SPARSE_TERNARY
: k == "uniform" ? UNIFORM_TERNARY
: SPARSE_ENCAPSULATED);
std::cout << "keydist: " << k << std::endl;
auto cc = GenCryptoContext(p);
cc->Enable(PKE);
cc->Enable(KEYSWITCH);
cc->Enable(LEVELEDSHE);
cc->Enable(ADVANCEDSHE);
cc->Enable(FHE);
std::cout << "ctx ok" << std::endl;
cc->EvalBootstrapSetup({3, 3}, {0, 0}, SLOTS);
std::cout << "setup ok" << std::endl;
auto keys = cc->KeyGen();
std::cout << "keygen ok" << std::endl;
cc->EvalMultKeyGen(keys.secretKey);
cc->EvalBootstrapKeyGen(keys.secretKey, SLOTS);
std::cout << "bootkeys ok" << std::endl;
#ifdef USE_FIDESLIB
cc->SetDevices({0}); // GPU 0; empty = broken CPU fallback
cc->SetAutoLoadPlaintexts(true);
cc->SetAutoLoadCiphertexts(true);
cc->LoadContext(keys.publicKey);
#endif
std::vector<double> xs(SLOTS);
for (uint32_t i = 0; i < SLOTS; i++)
xs[i] = -1.0 + 2.0 * i / (SLOTS - 1.0);
auto pt_in = cc->MakeCKKSPackedPlaintext(xs);
auto ct = cc->Encrypt(keys.publicKey, pt_in);
std::cout << "encrypt ok" << std::endl;
// Burn levels so the bootstrap is REAL (a fresh ciphertext is a no-op):
auto ones = cc->MakeCKKSPackedPlaintext(std::vector<double>(SLOTS, 1.0));
#ifdef USE_FIDESLIB
cc->LoadPlaintext(ones);
#endif
auto burn = ct;
while (burn->GetLevel() < 21) burn = cc->EvalMult(burn, ones);
std::cout << "input burned to level " << burn->GetLevel() << std::endl;
auto measure = [&](const char* tag, uint32_t iters, uint32_t prec) {
try {
auto b = burn;
auto t0 = std::chrono::steady_clock::now();
auto r = (iters == 1) ? cc->EvalBootstrap(b)
: cc->EvalBootstrap(b, iters, prec);
double ms = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - t0).count();
Plaintext pt;
cc->Decrypt(keys.secretKey, r, &pt);
pt->SetLength(SLOTS);
auto got = pt->GetRealPackedValue();
double worst = 0;
for (uint32_t i = 0; i < SLOTS; i += 61)
worst = std::max(worst, std::abs(got[i] - xs[i]));
std::cout << tag << ": level " << burn->GetLevel() << " -> "
<< r->GetLevel() << ", " << ms << " ms, max err "
<< worst << " (~2^" << std::log2(worst) << ")"
<< std::endl;
} catch (const std::exception& e) {
std::cout << tag << ": THROWS: " << e.what() << std::endl;
}
};
measure("1-iter", 1, 0);
measure("2-iter (precision 17)", 2, 17);
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(boot_precision_repro CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_BUILD_TYPE Release)
# Two builds from one source file:
# baseline: cmake -S . -B build-base -DOpenFHE_DIR=<stock 1.5.1>/lib/OpenFHE
# fideslib: cmake -S . -B build-fl -DUSE_FIDESLIB=ON \
# -Dfideslib_DIR=<prefix>/share/fideslib/cmake \
# -DOpenFHE_DIR=<FIDESlib's patched OpenFHE>/lib/OpenFHE
option(USE_FIDESLIB "Link FIDESlib instead of stock OpenFHE" OFF)
find_package(OpenFHE REQUIRED)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenFHE_CXX_FLAGS}")
add_executable(boot_precision_repro boot_precision_repro.cpp)
target_include_directories(boot_precision_repro PRIVATE
${OpenFHE_INCLUDE}
${OpenFHE_INCLUDE}/third-party/include
${OpenFHE_INCLUDE}/core
${OpenFHE_INCLUDE}/pke
${OpenFHE_INCLUDE}/binfhe)
target_link_libraries(boot_precision_repro PRIVATE ${OpenFHE_SHARED_LIBRARIES})
if(USE_FIDESLIB)
find_package(fideslib REQUIRED CONFIG)
target_compile_definitions(boot_precision_repro PRIVATE USE_FIDESLIB)
target_link_libraries(boot_precision_repro PRIVATE fideslib::fideslib)
endif()
We're happy to test candidate fixes on this hardware, or to provide any further measurements that would help. Thanks again for the library!
First of all, thank you for FIDESlib — the GPU performance is remarkable (we measured bootstrapping at ~10 ms and deg-31 Chebyshev evaluation at ~7 ms at N=2^16, versus seconds on CPU), and the OpenFHE-interoperable design made it straightforward to try against an existing workload. We hit the issue below while running a bootstrapped CKKS workload (an encrypted transformer inference) that was designed and validated against stock OpenFHE 1.5.1, and we hope the detailed repro makes it easy to pin down.
Summary
With
SPARSE_ENCAPSULATEDsecret keys,EvalBootstrapon GPU completes but returns results unrelated to the input (max error ≈ 1.0 on inputs in [-1, 1]), andEvalBootstrap(ct, 2, 17)crashes (SIGSEGV inside an OpenMP parallel region).UNIFORM_TERNARYbootstrapping also shows ~2^-1 error.SPARSE_TERNARYis correct (2^-9.8) with the identical harness and parameters, which we believe rules out a problem on our side.Notably, the patched OpenFHE that
deps/build.shinstalls handlesSPARSE_ENCAPSULATEDcorrectly when driven directly throughlbcrypto(2^-10.9 single-iteration, 2^-28.0 withnumIterations=2), so the issue appears to live in the FIDESlib layer rather than in the patched OpenFHE.Tested at
main@ 786c760 ("Fix issue #31").Measured matrix
Parameters: N = 2^16, depth 26, ScalingModSize 50, FirstModSize 51, FLEXIBLEAUTO, NumLargeDigits 4, HEStd_128_classic, 32768 slots,
EvalBootstrapSetup({3,3}, {0,0}, 32768), input linspace[-1, 1] burned to level 21 with plaintext multiplies so the bootstrap is real (not the fresh-ciphertext no-op).deps/build.sh, pure lbcrypto (CPU)Two smaller observations we can split into separate issues if you prefer:
EvalMult(const Ciphertext<DCRTPoly>&, Plaintext&)(api/CryptoContext.cpp:1220, taken whendevicesis empty) doesstd::any_cast<const lbcrypto::ConstPlaintext&>(pt->cpu), butMakeCKKSPackedPlaintextstores anlbcrypto::Plaintext; sincestd::any_castrequires an exact type match, this path always throwsstd::bad_any_cast.SPARSE_TERNARY(where bootstrapping works),numIterations = 2does not reduce the error (2^-9.76 → 2^-9.72), where stock OpenFHE improves 2^-11 → 2^-27.5 — iterated bootstrapping may not be wired through.Environment
main@ 786c760, built with-DFIDESLIB_INSTALL_OPENFHE=ON(its own patched OpenFHE), tests/benchmarks offSetDevices({0})Reproduce
One self-contained source file, built once against FIDESlib and once against the patched OpenFHE alone as the control. Build commands and the full expected-output matrix are in the README of the attached sample; the core is:
boot_precision_repro.cpp (click to expand)
CMakeLists.txt
We're happy to test candidate fixes on this hardware, or to provide any further measurements that would help. Thanks again for the library!