diff --git a/CMakeLists.txt b/CMakeLists.txt index d51afc78..c2e64715 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -225,6 +225,8 @@ target_sources(fideslib PUBLIC target_include_directories(fideslib PRIVATE nvtx/c/include + # api/ is the include root so headers use api-relative paths (e.g. "engine/cuda/CudaEngine.hpp"). + PUBLIC $ ) target_link_libraries(fideslib diff --git a/api/CCParams.cpp b/api/CCParams.cpp index 95b00779..eca36df1 100644 --- a/api/CCParams.cpp +++ b/api/CCParams.cpp @@ -4,84 +4,83 @@ #include #include +#include +#include namespace fideslib { CCParams::CCParams() { lbcrypto::CCParams params; - this->cpu = std::make_any>(std::move(params)); + this->host = std::make_any>(std::move(params)); } // ---- CKKS Parameters ---- void CCParams::SetMultiplicativeDepth(uint32_t depth) { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); params.SetMultiplicativeDepth(depth); } void CCParams::SetScalingModSize(uint32_t size) { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); params.SetScalingModSize(size); } void CCParams::SetBatchSize(uint32_t size) { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); params.SetBatchSize(size); } void CCParams::SetRingDim(uint32_t dim) { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); params.SetRingDim(dim); } void CCParams::SetScalingTechnique(ScalingTechnique tech) { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); auto scale_openfhe = static_cast(tech); assert((int)scale_openfhe == (int)tech); params.SetScalingTechnique(scale_openfhe); } void CCParams::SetNumLargeDigits(uint32_t numDigits) { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); params.SetNumLargeDigits(numDigits); } void CCParams::SetFirstModSize(uint32_t size) { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); params.SetFirstModSize(size); } void CCParams::SetDigitSize(uint32_t size) { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); params.SetDigitSize(size); } void CCParams::SetKeySwitchTechnique(KeySwitchTechnique tech) { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); auto ks_openfhe = static_cast(tech); assert((int)ks_openfhe == (int)tech); params.SetKeySwitchTechnique(ks_openfhe); } void CCParams::SetSecretKeyDist(SecretKeyDist dist) { - auto& params = std::any_cast&>(cpu); - - if (this->devices.empty()) { - if (dist == SecretKeyDist::SPARSE_TERNARY) { - params.SetSecretKeyDist(lbcrypto::SPARSE_TERNARY); - } - else { - params.SetSecretKeyDist(lbcrypto::UNIFORM_TERNARY); - } - } - else { + auto& params = std::any_cast&>(host); + + // Record the requested distribution as-is. The CUDA backend's restriction against sparse-ternary keys + // is enforced at context construction (GenCryptoContext), so an unsupported choice fails loudly + // there rather than being silently downgraded here. + if (dist == SecretKeyDist::SPARSE_TERNARY) { + params.SetSecretKeyDist(lbcrypto::SPARSE_TERNARY); + } else { params.SetSecretKeyDist(lbcrypto::UNIFORM_TERNARY); } keyDist = dist; } void CCParams::SetSecurityLevel(SecurityLevel level) { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); auto sl_openfhe = static_cast(level); assert((int)sl_openfhe == (int)level); params.SetSecurityLevel(sl_openfhe); @@ -90,25 +89,34 @@ void CCParams::SetSecurityLevel(SecurityLevel level) { // ---- Getters ---- SecretKeyDist CCParams::GetSecretKeyDist() const { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); auto skd_openfhe = params.GetSecretKeyDist(); return static_cast(skd_openfhe); } uint32_t CCParams::GetMultiplicativeDepth() const { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); return params.GetMultiplicativeDepth(); } uint32_t CCParams::GetBatchSize() const { - auto& params = std::any_cast&>(cpu); + auto& params = std::any_cast&>(host); return params.GetBatchSize(); } -// ---- Device Parameters ---- +// ---- Backend Parameters ---- -void CCParams::SetDevices(std::vector&& devices) { - this->devices = std::move(devices); +void CCParams::SetBackend(Backend backend) { + if (!IsBackendAvailable(backend)) { + // Build a descriptive name for the unavailable backend. + const char* name = "unknown"; + switch (backend) { + case Backend::CPU: name = "CPU"; break; + case Backend::CUDA: name = "CUDA"; break; + } + OPENFHE_THROW(std::string(name) + " backend requested but not available (FIDESlib was not built with support for this backend)"); + } + this->backend = backend; } void CCParams::SetPlaintextAutoload(bool autoload) { diff --git a/api/CCParams.hpp b/api/CCParams.hpp index 55e5c02c..55ecabe4 100644 --- a/api/CCParams.hpp +++ b/api/CCParams.hpp @@ -7,6 +7,7 @@ #include #include "Definitions.hpp" +#include "engine/Backend.hpp" namespace fideslib { @@ -44,9 +45,11 @@ template <> class CCParams { void SetSecretKeyDist(SecretKeyDist dist); void SetSecurityLevel(SecurityLevel level); - // ---- Device Parameters ---- + // ---- Backend Parameters ---- - void SetDevices(std::vector&& devices); + /// @brief Select the execution backend (default CPU). Multi-GPU device selection is done + /// later through the facade method CryptoContextImpl::SetCudaDevices, before LoadContext. + void SetBackend(Backend backend); void SetPlaintextAutoload(bool autoload); void SetCiphertextAutoload(bool autoload); @@ -57,11 +60,11 @@ template <> class CCParams { // ---- Internal State ---- - std::any cpu; - std::vector devices = { }; - SecretKeyDist keyDist = UNIFORM_TERNARY; - bool plaintextAutoload = false; - bool ciphertextAutoload = true; + std::any host; + Backend backend = Backend::CPU; + SecretKeyDist keyDist = UNIFORM_TERNARY; + bool plaintextAutoload = false; + bool ciphertextAutoload = true; }; } // namespace fideslib diff --git a/api/Ciphertext.cpp b/api/Ciphertext.cpp index a3a422b5..682271ae 100644 --- a/api/Ciphertext.cpp +++ b/api/Ciphertext.cpp @@ -1,5 +1,4 @@ #include "Ciphertext.hpp" -#include "CKKS/Ciphertext.cuh" #include "Definitions.hpp" #include @@ -7,13 +6,6 @@ namespace fideslib { -CiphertextImpl::~CiphertextImpl() { - if (this->loaded && this->gpu != 0 && this->parent_context) { - this->parent_context->EvictDeviceCiphertext(this->gpu); - this->gpu = 0; - } -} - CiphertextImpl::CiphertextImpl(const CryptoContext&& context) : parent_context(context) { if (!context) { OPENFHE_THROW("Cannot create Ciphertext with null CryptoContext"); @@ -23,22 +15,17 @@ CiphertextImpl::CiphertextImpl(const CryptoContext&& context // ---- Copy ---- CiphertextImpl::CiphertextImpl(const CiphertextImpl& other) { - // Share CPU ciphertext and detach only on first CPU mutation. - auto const& other_cpu = std::any_cast&>(other.cpu); - this->cpu = std::make_any>(other_cpu); - this->need_lazy_copy = true; - - // Copy underlying GPU ciphertext if loaded. - this->loaded = other.loaded; - if (this->loaded) { - this->gpu = other.parent_context->CopyDeviceCiphertext(other); - } else { - this->gpu = 0; - } + // Share host ciphertext and detach only on first host mutation. + auto const& other_host = std::any_cast&>(other.host); + this->host = std::make_any>(other_host); + this->need_lazy_copy = true; // Copy parent context. this->parent_context = other.parent_context; - this->original_level = other.original_level; + + // Cloning any backend-resident payload is the engine's decision, not ours: it returns an empty + // slot when `other` is not resident (always so on the CPU backend). + this->device = other.parent_context->CloneCiphertextBackend(other); } CiphertextImpl::CiphertextImpl(const Ciphertext& other) : CiphertextImpl(static_cast&>(other)) { @@ -51,94 +38,78 @@ Ciphertext CiphertextImpl::Clone() const { return clone; } -// ---- Getters ---- +// ---- Getters / setters ---- +// +// These delegate unconditionally to the backend; the engine decides whether to read the host value or +// the device-resident copy. The host primitives below hold the host computation the engines reuse. size_t CiphertextImpl::GetLevel() const { - - if (!this->loaded) { - // Fall back to CPU. - auto& ct = std::any_cast&>(this->cpu); - return ct->GetLevel(); - } - - // GPU path. Depth is reversed in FIDESlib, must do depth = maxDepth - depth - auto ct_gpu = std::static_pointer_cast(this->parent_context->GetDeviceCiphertext(this->gpu)); - auto maxDepth = this->parent_context->multiplicative_depth; - return maxDepth - ct_gpu->getLevel(); + return this->parent_context->CiphertextLevel(*this); } size_t CiphertextImpl::GetNoiseScaleDeg() const { + return this->parent_context->CiphertextNoiseScaleDeg(*this); +} - if (!this->loaded) { - // Fall back to CPU. - auto& ct = std::any_cast&>(this->cpu); - return ct->GetNoiseScaleDeg(); - } +void CiphertextImpl::SetSlots(size_t slots) { + this->parent_context->SetCiphertextSlots(*this, slots); +} - // GPU path. - auto ct_gpu = std::static_pointer_cast(this->parent_context->GetDeviceCiphertext(this->gpu)); - return ct_gpu->NoiseLevel; +void CiphertextImpl::SetLevel(size_t level) { + this->parent_context->SetCiphertextLevel(*this, level); } -// ---- Setters ---- +// ---- Host primitives (invoked by the engine backends; no backend logic of their own) ---- -void CiphertextImpl::SetSlots(size_t slots) { +size_t CiphertextImpl::GetLevelHost() const { + auto& ct = std::any_cast&>(this->host); + return ct->GetLevel(); +} - if (!this->loaded) { - // Fall back to CPU. - this->EnsureLazyCPUCopy(); - auto& ct = std::any_cast&>(this->cpu); - ct->SetSlots(slots); - return; - } - // GPU path. - auto ct_gpu = std::static_pointer_cast(this->parent_context->GetDeviceCiphertext(this->gpu)); - ct_gpu->slots = static_cast(slots); +size_t CiphertextImpl::GetNoiseScaleDegHost() const { + auto& ct = std::any_cast&>(this->host); + return ct->GetNoiseScaleDeg(); } -void CiphertextImpl::SetLevel(size_t level) { +void CiphertextImpl::SetSlotsHost(size_t slots) { + this->EnsureLazyHostCopy(); + auto& ct = std::any_cast&>(this->host); + ct->SetSlots(slots); +} - if (!this->loaded) { - // Fall back to CPU. - this->EnsureLazyCPUCopy(); - auto& ct = std::any_cast&>(this->cpu); +void CiphertextImpl::SetLevelHost(size_t level) { + this->EnsureLazyHostCopy(); + auto& ct = std::any_cast&>(this->host); - size_t currentTowers = ct->GetElements()[0].GetNumOfElements(); - size_t currentLevel = ct->GetLevel(); + size_t currentTowers = ct->GetElements()[0].GetNumOfElements(); + size_t currentLevel = ct->GetLevel(); - size_t totalPrimes = currentTowers + currentLevel; - size_t targetTowers = totalPrimes - level; + size_t totalPrimes = currentTowers + currentLevel; + size_t targetTowers = totalPrimes - level; - if (currentTowers > targetTowers) { - // Need to drop towers - size_t towersToDrop = currentTowers - targetTowers; + if (currentTowers > targetTowers) { + // Need to drop towers + size_t towersToDrop = currentTowers - targetTowers; - auto& elements = ct->GetElements(); - for (auto& elem : elements) { - elem.DropLastElements(towersToDrop); - } + auto& elements = ct->GetElements(); + for (auto& elem : elements) { + elem.DropLastElements(towersToDrop); } - - ct->SetLevel(level); - - return; } - // GPU path. - auto ct_gpu = std::static_pointer_cast(this->parent_context->GetDeviceCiphertext(this->gpu)); - auto maxDepth = this->parent_context->multiplicative_depth; - ct_gpu->dropToLevel(maxDepth - level); + ct->SetLevel(level); } -void CiphertextImpl::EnsureLazyCPUCopy() { - if (!this->need_lazy_copy) { +void CiphertextImpl::EnsureLazyHostCopy() { + auto const& ct_host = std::any_cast&>(this->host); + // Detach before in-place mutation if still lazily shared or the underlying + // OpenFHE ciphertext has other owners (e.g. a Clone): copy-on-write. + if (!this->need_lazy_copy && ct_host.use_count() <= 1) { return; } - - auto const& ct_cpu = std::any_cast&>(this->cpu); - lbcrypto::Ciphertext cpu_copy = std::make_shared>(*ct_cpu); - this->cpu = std::make_any>(std::move(cpu_copy)); - this->need_lazy_copy = false; + lbcrypto::Ciphertext host_copy = std::make_shared>(*ct_host); + this->host = std::make_any>(std::move(host_copy)); + this->need_lazy_copy = false; } // ---- Operators ---- diff --git a/api/Ciphertext.hpp b/api/Ciphertext.hpp index 75e1a7a1..68a552ca 100644 --- a/api/Ciphertext.hpp +++ b/api/Ciphertext.hpp @@ -4,8 +4,8 @@ #include #include -#include "Definitions.hpp" #include "CryptoContext.hpp" +#include "Definitions.hpp" namespace fideslib { @@ -13,7 +13,7 @@ namespace fideslib { template <> class CiphertextImpl { public: CiphertextImpl() = delete; - ~CiphertextImpl(); + ~CiphertextImpl() = default; // device slot frees its backend payload via RAII CiphertextImpl(const CryptoContext&& context); @@ -34,27 +34,30 @@ template <> class CiphertextImpl { // ---- Clone ---- Ciphertext Clone() const; - // ---- Getters ---- + // ---- Getters / setters (delegate to the backend) ---- - size_t GetLevel() const; + size_t GetLevel() const; size_t GetNoiseScaleDeg() const; - - // ---- Setters ---- void SetSlots(size_t slots); void SetLevel(size_t level); - void EnsureLazyCPUCopy(); + + // ---- Host primitives (the host computation the engine backends reuse; no backend logic) ---- + + size_t GetLevelHost() const; + size_t GetNoiseScaleDegHost() const; + void SetSlotsHost(size_t slots); + void SetLevelHost(size_t level); + void EnsureLazyHostCopy(); // ---- Internal State ---- bool need_lazy_copy = false; - std::any cpu; - uint32_t gpu = 0; - /// @brief Flag indicating whether the ciphertext is loaded to the devices. - bool loaded = false; + /// @brief Canonical host value (lbcrypto::Ciphertext); used by host ops and as the CUDA readback shadow. + std::any host; + /// @brief Backend-resident payload (the CUDA backend stores a shared_ptr to its device ciphertext); empty == not resident. + std::any device; /// @brief Parent context. CryptoContext parent_context; - /// @brief Original level of the ciphertext when loaded. - size_t original_level = 0; }; // ---- Override Operators ---- diff --git a/api/CryptoContext.cpp b/api/CryptoContext.cpp index f474f5bc..78dd6172 100644 --- a/api/CryptoContext.cpp +++ b/api/CryptoContext.cpp @@ -1,25 +1,18 @@ #include "CryptoContext.hpp" -#include "CKKS/AccumulateBroadcast.cuh" -#include "CKKS/ApproxModEval.cuh" -#include "CKKS/Bootstrap.cuh" -#include "CKKS/Ciphertext.cuh" -#include "CKKS/Context.cuh" -#include "CKKS/KeySwitchingKey.cuh" -#include "CKKS/LinearTransform.cuh" -#include "CKKS/Parameters.cuh" -#include "CKKS/Plaintext.cuh" -#include "CKKS/forwardDefs.cuh" -#include "CKKS/openfhe-interface/RawCiphertext.cuh" -#include "CudaUtils.cuh" + +#include "HostMath.hpp" + #include "Definitions.hpp" -#include "PolyApprox.cuh" #include "PublicKey.hpp" #include "Serialize.hpp" #include "ciphertext-fwd.h" #include "cryptocontext-fwd.h" +#include "engine/Engine.hpp" +#include "engine/EngineCommon.hpp" #include "lattice/hal/lat-backend.h" #include +#include #include #include #include @@ -42,76 +35,6 @@ std::map& ct) { - if (ct->need_lazy_copy) { - ct->EnsureLazyCPUCopy(); - } -} -} // namespace - -static std::vector p64{ { .p = 2305843009218281473 }, - { .p = 2251799661248513 }, - { .p = 2251799661641729 }, - { .p = 2251799665180673 }, - { .p = 2251799682088961 }, - { .p = 2251799678943233 }, - { .p = 2251799717609473 }, - { .p = 2251799710138369 }, - { .p = 2251799708827649 }, - { .p = 2251799707385857 }, - { .p = 2251799713677313 }, - { .p = 2251799712366593 }, - { .p = 2251799716691969 }, - { .p = 2251799714856961 }, - { .p = 2251799726522369 }, - { .p = 2251799726129153 }, - { .p = 2251799747493889 }, - { .p = 2251799741857793 }, - { .p = 2251799740416001 }, - { .p = 2251799746707457 }, - { .p = 2251799756013569 }, - { .p = 2251799775805441 }, - { .p = 2251799763091457 }, - { .p = 2251799767154689 }, - { .p = 2251799765975041 }, - { .p = 2251799770562561 }, - { .p = 2251799769776129 }, - { .p = 2251799772266497 }, - { .p = 2251799775281153 }, - { .p = 2251799774887937 }, - { .p = 2251799797432321 }, - { .p = 2251799787995137 }, - { .p = 2251799787601921 }, - { .p = 2251799791403009 }, - { .p = 2251799789568001 }, - { .p = 2251799795466241 }, - { .p = 2251799807131649 }, - { .p = 2251799806345217 }, - { .p = 2251799805165569 }, - { .p = 2251799813554177 }, - { .p = 2251799809884161 }, - { .p = 2251799810670593 }, - { .p = 2251799818928129 }, - { .p = 2251799816568833 }, - { .p = 2251799815520257 } }; - -static std::vector sp64{ { .p = 2305843009218936833 }, - { .p = 2305843009220116481 }, - { .p = 2305843009221820417 }, - { .p = 2305843009224179713 }, - { .p = 2305843009225228289 }, - { .p = 2305843009227980801 }, - { .p = 2305843009229160449 }, - { .p = 2305843009229946881 }, - { .p = 2305843009231650817 }, - { .p = 2305843009235189761 }, - { .p = 2305843009240301569 }, - { .p = 2305843009242923009 }, - { .p = 2305843009244889089 }, - { .p = 2305843009245413377 }, - { .p = 2305843009247641601 } }; - static std::unordered_map PKESchemeFeatureMap = { { PKESchemeFeature::PKE, lbcrypto::PKE }, { PKESchemeFeature::KEYSWITCH, lbcrypto::KEYSWITCH }, @@ -124,11 +47,10 @@ static std::unordered_map PKESchem }; CryptoContextImpl::~CryptoContextImpl() { - FIDESlib::CudaNvtxRange r("API"); - if (this->loaded) { - auto& context_gpu = std::any_cast(this->gpu); - FIDESlib::CKKS::DeregisterCryptoContextGPU(context_gpu); - this->gpu = std::any(); + // Tear down backend state (e.g. GPU context) before clearing the OpenFHE eval keys. + // engine_ is null on a moved-from context. + if (engine_) { + engine_->teardown(); } lbcrypto::CryptoContextImpl>>>::ClearEvalMultKeys(); lbcrypto::CryptoContextImpl>>>::ClearEvalAutomorphismKeys(); @@ -137,157 +59,98 @@ CryptoContextImpl::~CryptoContextImpl() { // ---- Enable features ---- void CryptoContextImpl::Enable(PKESchemeFeature feature) { - FIDESlib::CudaNvtxRange r("API"); - auto& context = std::any_cast&>(this->cpu); + auto& context = std::any_cast&>(this->host); context->Enable(PKESchemeFeatureMap[feature]); } void CryptoContextImpl::Enable(uint32_t featureMask) { - FIDESlib::CudaNvtxRange r("API"); - auto& context = std::any_cast&>(this->cpu); + auto& context = std::any_cast&>(this->host); context->Enable(featureMask); } // ---- Getters ---- uint32_t CryptoContextImpl::GetCyclotomicOrder() const { - FIDESlib::CudaNvtxRange r("API"); - auto& context = std::any_cast&>(this->cpu); + auto& context = std::any_cast&>(this->host); return context->GetCyclotomicOrder(); } uint32_t CryptoContextImpl::GetRingDimension() const { - FIDESlib::CudaNvtxRange r("API"); - auto& context = std::any_cast&>(this->cpu); + auto& context = std::any_cast&>(this->host); return context->GetRingDimension(); } -double CryptoContextImpl::GetPreScaleFactor(uint32_t slots) { - FIDESlib::CudaNvtxRange r("API"); - if (!this->loaded) { - FIDESlib::CudaNvtxRange r("API"); - OPENFHE_THROW("CryptoContext not loaded to any device"); +double CryptoContextImpl::GetPreScaleFactor(uint32_t /*slots*/) { + // Pure host computation (a single scalar derived from the crypto parameters + the bootstrap + // correction factor); it touches no device, so it lives here rather than on the engine. This + // reproduces the GPU GetPreScaleFactor exactly, reading the OpenFHE host parameters in place of + // that function's device-mirrored copies (see engine-host-audit.md). `slots` is unused: + // OpenFHE keeps a single m_correctionFactor (the last EvalBootstrapSetup wins), which is what the + // device copy mirrors — callers set up bootstrap for one slot count. + auto& context = std::any_cast&>(this->host); + const auto cryptoParams = std::dynamic_pointer_cast(context->GetCryptoParameters()); + const auto& elementParam = cryptoParams->GetElementParams()->GetParams(); + const size_t L = elementParam.size() - 1; + + const double qDouble = elementParam[0]->GetModulus().ConvertToDouble(); // q0 (FIDESlib prime[0]) + const double powP = std::pow(2.0, static_cast(cryptoParams->GetPlaintextModulus())); + const int32_t deg = static_cast(std::round(std::log2(qDouble / powP))); + + const uint32_t correctionFactor = std::dynamic_pointer_cast(context->GetScheme()->m_FHE)->GetCKKSBootCorrectionFactor(); + const uint32_t correction = correctionFactor - deg; + + const auto st = cryptoParams->GetScalingTechnique(); + if (st == lbcrypto::FLEXIBLEAUTO || st == lbcrypto::FLEXIBLEAUTOEXT) { + const uint32_t lvl = (st == lbcrypto::FLEXIBLEAUTOEXT) ? 1U : 0U; + const double targetSF = cryptoParams->GetScalingFactorReal(lvl); // FIDESlib SFReal[L-lvl] + const double sourceSF = cryptoParams->GetScalingFactorReal(L - 1); // FIDESlib SFReal[1] + const double modToDrop = elementParam[1]->GetModulus().ConvertToDouble(); // FIDESlib prime[1] + double adjustmentFactor = (targetSF / sourceSF) * (modToDrop / sourceSF); + adjustmentFactor *= std::pow(2.0, -1.0 * static_cast(correction)); + return adjustmentFactor; } - auto& context_gpu = std::any_cast(this->gpu); - return FIDESlib::CKKS::GetPreScaleFactor(context_gpu, static_cast(slots)); + return std::pow(2.0, -1.0 * static_cast(correction)); } // ---- Setters ---- void CryptoContextImpl::SetAutoLoadPlaintexts(bool autoload) { - FIDESlib::CudaNvtxRange r("API"); this->auto_load_plaintexts = autoload; } void CryptoContextImpl::SetAutoLoadCiphertexts(bool autoload) { - FIDESlib::CudaNvtxRange r("API"); this->auto_load_ciphertexts = autoload; } -void CryptoContextImpl::SetDevices(const std::vector& devices) { - FIDESlib::CudaNvtxRange r("API"); - if (this->loaded) { - FIDESlib::CudaNvtxRange r("API"); - OPENFHE_THROW("SetDevices must be called before LoadContext"); +void CryptoContextImpl::SetCudaDevices(const std::vector& devices) { + if (engine_->isContextLoaded()) { + OPENFHE_THROW("SetCudaDevices must be called before LoadContext"); } + engine_->setDevices(devices); +} - this->devices = devices; +std::vector CryptoContextImpl::GetCudaDevices() const { + return engine_->devices(); } // ---- Load to devices ---- void CryptoContextImpl::LoadContext(const PublicKey& publicKey) { - FIDESlib::CudaNvtxRange r("API"); - if (this->loaded || this->devices.empty()) - return; - - auto& context = std::any_cast&>(this->cpu); - FIDESlib::CKKS::Parameters params{ .logN = 16, .L = 6, .dnum = 2, .primes = std::vector(p64), .Sprimes = std::vector(sp64), .batch = 100 }; - - // Determine the boot configuration based on the secret key distribution. - const auto cryptoParams = std::dynamic_pointer_cast(context->GetCryptoParameters()); - FIDESlib::BOOT_CONFIG bootConfig; - switch (this->keyDist) { - case fideslib::UNIFORM_TERNARY: bootConfig = FIDESlib::UNIFORM; break; - case fideslib::SPARSE_TERNARY: bootConfig = FIDESlib::SPARSE; break; - case fideslib::SPARSE_ENCAPSULATED: bootConfig = FIDESlib::ENCAPS; break; - default: bootConfig = FIDESlib::UNIFORM; break; - } - - FIDESlib::CKKS::RawParams rawParams = FIDESlib::CKKS::GetRawParams(context, bootConfig); - params = params.adaptTo(rawParams); - FIDESlib::CKKS::Context c = FIDESlib::CKKS::GenCryptoContextGPU(params, this->devices); - - auto& pkImpl = std::any_cast&>(publicKey->pimpl); - - // Multiplicative key switching key. - auto& keyMap = context->GetAllEvalMultKeys(); // lbcrypto::CryptoContextImpl::s_evalMultKeyMap; - if (keyMap.find(pkImpl->GetKeyTag()) != keyMap.end()) { - auto raw_eval_ksk = FIDESlib::CKKS::GetEvalKeySwitchKey(pkImpl); - FIDESlib::CKKS::KeySwitchingKey eval_ksk(c); - eval_ksk.Initialize(raw_eval_ksk); - c->AddEvalKey(std::move(eval_ksk)); - } - // Rotational key switching keys. - for (const auto& step : this->rotation_indexes) { - auto raw_rot_ksk = FIDESlib::CKKS::GetRotationKeySwitchKey(pkImpl, step); - FIDESlib::CKKS::KeySwitchingKey rot_ksk(c); - rot_ksk.Initialize(raw_rot_ksk); - c->AddRotationKey(step, std::move(rot_ksk)); - } - // Bootstrapping keys. - for (const auto& slot : this->slots_bootstrap) { - FIDESlib::CKKS::AddBootstrapPrecomputation(pkImpl, slot, c); - } - - this->gpu = std::make_any(std::move(c)); - this->loaded = true; + engine_->loadContext(*this, publicKey); } void CryptoContextImpl::LoadPlaintext(Plaintext& pt) { - FIDESlib::CudaNvtxRange r("API"); - if (pt->loaded || this->devices.empty()) - return; - - if (!this->loaded) { - OPENFHE_THROW("CryptoContext not loaded to any device"); - } - - auto& context_gpu = std::any_cast(this->gpu); - auto& context = std::any_cast&>(this->cpu); - const auto& ptImpl = std::any_cast(pt->cpu); - FIDESlib::CKKS::RawPlainText raw_pt = FIDESlib::CKKS::GetRawPlainText(context, ptImpl); - std::shared_ptr gpu_pt = std::make_shared(context_gpu, raw_pt); - uint32_t handle = this->RegisterDevicePlaintext(std::move(gpu_pt)); - pt->gpu = handle; - pt->loaded = true; + engine_->loadPlaintext(*this, pt); } void CryptoContextImpl::LoadCiphertext(Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); - if (ct->loaded || this->devices.empty()) - return; - - if (!this->loaded) { - OPENFHE_THROW("CryptoContext not loaded to any device"); - } - - auto& context_gpu = std::any_cast(this->gpu); - auto& context = std::any_cast&>(this->cpu); - const auto& ctImpl = std::any_cast&>(ct->cpu); - FIDESlib::CKKS::RawCipherText raw_ct = FIDESlib::CKKS::GetRawCipherText(context, ctImpl); - std::shared_ptr gpu_ct = std::make_shared(context_gpu, raw_ct); - uint32_t handle = this->RegisterDeviceCiphertext(std::move(gpu_ct)); - ct->gpu = handle; - ct->loaded = true; - ct->original_level = this->multiplicative_depth - ct->GetLevel(); + engine_->loadCiphertext(*this, ct); } // ---- Key Generation ---- KeyPair CryptoContextImpl::KeyGen() { - FIDESlib::CudaNvtxRange r("API"); - auto& context = std::any_cast&>(this->cpu); + auto& context = std::any_cast&>(this->host); auto keys = context->KeyGen(); KeyPair keypair; @@ -301,25 +164,23 @@ KeyPair CryptoContextImpl::KeyGen() { } void CryptoContextImpl::EvalMultKeyGen(const PrivateKey& sk) { - FIDESlib::CudaNvtxRange r("API"); - if (!this->devices.empty() && this->loaded) { + if (engine_->isContextLoaded()) { OPENFHE_THROW("EvalMultKeyGen must be called before LoadContext"); } - auto& context = std::any_cast&>(this->cpu); + auto& context = std::any_cast&>(this->host); auto& skImpl = std::any_cast&>(sk->pimpl); context->EvalMultKeyGen(skImpl); } void CryptoContextImpl::EvalRotateKeyGen(const PrivateKey& sk, const std::vector& steps) { - FIDESlib::CudaNvtxRange r("API"); - if (!this->devices.empty() && this->loaded) { + if (engine_->isContextLoaded()) { OPENFHE_THROW("EvalRotateKeyGen must be called before LoadContext"); } - auto& context = std::any_cast&>(this->cpu); + auto& context = std::any_cast&>(this->host); auto& skImpl = std::any_cast&>(sk->pimpl); context->EvalRotateKeyGen(skImpl, steps); this->rotation_indexes.insert(this->rotation_indexes.end(), steps.begin(), steps.end()); @@ -328,57 +189,24 @@ void CryptoContextImpl::EvalRotateKeyGen(const PrivateKey& s // ---- Bootstrapping ---- void CryptoContextImpl::EvalBootstrapSetup(const std::vector& levelBudget, std::vector dim1, uint32_t slots, uint32_t correctionFactor, bool precompute, bool btsfirstboot) { - FIDESlib::CudaNvtxRange r("API"); - - // Only before loading one must compute the bootstrapping auxiliary data. - if (this->loaded) { - OPENFHE_THROW("Context is already loaded"); - }auto& context = std::any_cast&>(this->cpu); - - std::vector coeffchebyshev; - int doubleAngleIts = 3; - - if (this->keyDist == fideslib::SPARSE_ENCAPSULATED) { - coeffchebyshev = lbcrypto::FHECKKSRNS::g_coefficientsSparseEncapsulated; - doubleAngleIts = lbcrypto::FHECKKSRNS::R_SPARSE; - } else if (this->keyDist == fideslib::SPARSE_TERNARY) { - coeffchebyshev = lbcrypto::FHECKKSRNS::g_coefficientsSparse; - doubleAngleIts = lbcrypto::FHECKKSRNS::R_SPARSE; - } else if (this->keyDist == fideslib::UNIFORM_TERNARY) { - coeffchebyshev = lbcrypto::FHECKKSRNS::g_coefficientsUniform; - doubleAngleIts = lbcrypto::FHECKKSRNS::R_UNIFORM; - } else { - OPENFHE_THROW("Unsupported key distribution"); - } - - int32_t modall = static_cast(lbcrypto::GetMultiplicativeDepthByCoeffVector(coeffchebyshev, false)) + doubleAngleIts; - - if (this->devices.empty()) { - context->EvalBootstrapSetup(levelBudget, std::move(dim1), slots, correctionFactor, true); - return; + // Bootstrap setup is a host (OpenFHE) computation; the facade performs it and the engine only + // supplies the per-backend argument policy. + if (engine_->isContextLoaded()) { + OPENFHE_THROW("EvalBootstrapSetup must be called before LoadContext"); } - - context->EvalBootstrapSetup(levelBudget, std::move(dim1), slots, correctionFactor, precompute, btsfirstboot, modall); + auto& context = std::any_cast&>(this->host); + int32_t modall = bootstrapModEvalLevels(this->keyDist); + BootstrapSetupPolicy p = engine_->bootstrapSetupPolicy(precompute, btsfirstboot, modall); + context->EvalBootstrapSetup(levelBudget, std::move(dim1), slots, correctionFactor, p.precompute, p.btSlotsEncoding, p.modEvalLevels); } void CryptoContextImpl::EvalBootstrapKeyGen(const PrivateKey& secretKey, uint32_t slots) { - FIDESlib::CudaNvtxRange r("API"); - - if (this->loaded) { - OPENFHE_THROW("Context is already loaded"); - } - - auto& skImpl = std::any_cast&>(secretKey->pimpl); - - this->slots_bootstrap.push_back(slots); - - FIDESlib::CKKS::GenBootstrapKeys(skImpl, slots, this->keyDist == fideslib::SPARSE_ENCAPSULATED); + engine_->evalBootstrapKeyGen(*this, secretKey, slots); } // ---- Serialization ---- bool CryptoContextImpl::SerializeEvalMultKey(std::ostream& ser, const fideslib::SerType& sertype, const std::string& keyTag) { - FIDESlib::CudaNvtxRange r("API"); bool res; switch (sertype) { case fideslib::SerType::BINARY: @@ -396,7 +224,6 @@ bool CryptoContextImpl::SerializeEvalMultKey(std::ostream& ser, const } bool CryptoContextImpl::SerializeEvalAutomorphismKey(std::ostream& ser, const SerType& sertype, const std::string& keyTag) { - FIDESlib::CudaNvtxRange r("API"); bool res; switch (sertype) { case SerType::BINARY: @@ -416,9 +243,8 @@ bool CryptoContextImpl::SerializeEvalAutomorphismKey(std::ostream& ser // ---- Deserialization ---- bool CryptoContextImpl::DeserializeEvalMultKey(std::istream& ser, const SerType& sertype) const { - FIDESlib::CudaNvtxRange r("API"); - if (!this->devices.empty() && this->loaded) { + if (engine_->isContextLoaded()) { OPENFHE_THROW("DeserializeEvalMultKey must be called before LoadContext"); } @@ -439,9 +265,8 @@ bool CryptoContextImpl::DeserializeEvalMultKey(std::istream& ser, cons } bool CryptoContextImpl::DeserializeEvalAutomorphismKey(std::istream& ser, const SerType& sertype) const { - FIDESlib::CudaNvtxRange r("API"); - if (!this->devices.empty() && this->loaded) { + if (engine_->isContextLoaded()) { OPENFHE_THROW("DeserializeEvalAutomorphismKey must be called before LoadContext"); } @@ -468,16 +293,14 @@ Plaintext CryptoContextImpl::MakeCKKSPackedPlaintext(const std::vector uint32_t level, const std::shared_ptr params, uint32_t slots) { - FIDESlib::CudaNvtxRange r("API"); - auto& context = std::any_cast&>(this->cpu); + auto& context = std::any_cast&>(this->host); auto pt = context->MakeCKKSPackedPlaintext(value, noiseScaleDeg, level, nullptr, slots); Plaintext plaintext = std::make_shared(this->self_reference.lock()); - plaintext->cpu = std::make_any(pt); - plaintext->loaded = false; + plaintext->host = std::make_any(pt); - if (this->devices.empty() || !this->auto_load_plaintexts) { + if (!this->auto_load_plaintexts) { return plaintext; } @@ -488,16 +311,14 @@ Plaintext CryptoContextImpl::MakeCKKSPackedPlaintext(const std::vector Plaintext CryptoContextImpl::MakeCKKSPackedPlaintext(const std::vector& value, size_t noiseScaleDeg, uint32_t level, const std::shared_ptr params, uint32_t slots) { - FIDESlib::CudaNvtxRange r("API"); - auto& context = std::any_cast&>(this->cpu); + auto& context = std::any_cast&>(this->host); auto pt = context->MakeCKKSPackedPlaintext(value, noiseScaleDeg, level, nullptr, slots); Plaintext plaintext = std::make_shared(this->self_reference.lock()); - plaintext->cpu = std::make_any(pt); - plaintext->loaded = false; + plaintext->host = std::make_any(pt); - if (this->devices.empty() || !this->auto_load_plaintexts) { + if (!this->auto_load_plaintexts) { return plaintext; } @@ -509,17 +330,16 @@ CryptoContextImpl::MakeCKKSPackedPlaintext(const std::vector& // ---- Encryption ---- Ciphertext CryptoContextImpl::Encrypt(Plaintext& pt, const PublicKey& pk) { - FIDESlib::CudaNvtxRange r("API"); - auto& context = std::any_cast&>(this->cpu); + auto& context = std::any_cast&>(this->host); const auto& pkImpl = std::any_cast&>(pk->pimpl); - const auto& ptImpl = std::any_cast(pt->cpu); + const auto& ptImpl = std::any_cast(pt->host); auto ct = context->Encrypt(pkImpl, ptImpl); Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); + ciphertext->host = std::make_any>(ct); - if (this->devices.empty() || !this->auto_load_ciphertexts) { + if (!this->auto_load_ciphertexts) { return ciphertext; } @@ -529,22 +349,20 @@ Ciphertext CryptoContextImpl::Encrypt(Plaintext& pt, const P } Ciphertext CryptoContextImpl::Encrypt(const PublicKey& pk, Plaintext& pt) { - FIDESlib::CudaNvtxRange r("API"); return Encrypt(pt, pk); } Ciphertext CryptoContextImpl::Encrypt(Plaintext& pt, const PrivateKey& sk) { - FIDESlib::CudaNvtxRange r("API"); - auto& context = std::any_cast&>(this->cpu); + auto& context = std::any_cast&>(this->host); const auto& skImpl = std::any_cast&>(sk->pimpl); - const auto& ptImpl = std::any_cast(pt->cpu); + const auto& ptImpl = std::any_cast(pt->host); auto ct = context->Encrypt(skImpl, ptImpl); Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); + ciphertext->host = std::make_any>(ct); - if (this->devices.empty() || !this->auto_load_ciphertexts) { + if (!this->auto_load_ciphertexts) { return ciphertext; } @@ -554,68 +372,32 @@ Ciphertext CryptoContextImpl::Encrypt(Plaintext& pt, const P } Ciphertext CryptoContextImpl::Encrypt(const PrivateKey& sk, Plaintext& pt) { - FIDESlib::CudaNvtxRange r("API"); return Encrypt(pt, sk); } DecryptResult CryptoContextImpl::Decrypt(Ciphertext& ct, const PrivateKey& sk, Plaintext* pt) { - FIDESlib::CudaNvtxRange r("API"); - if (pt == nullptr) { OPENFHE_THROW("Plaintext pointer is null"); } + // The only backend-specific step is the readback (device->host); it is its own engine hook + // (no-op on CPU). The decrypt itself is a host OpenFHE computation, so it lives here. + engine_->recoverHostCiphertext(*this, ct); - auto& context = std::any_cast&>(this->cpu); - - if (ct->loaded) { - EnsureMutableCpuCiphertext(ct); - } - - auto& ct_cpu = std::any_cast&>(ct->cpu); - - // Copy ciphertext to CPU if needed. - if (ct->loaded) { - auto ct_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - FIDESlib::CKKS::RawCipherText raw_ct; - ct_gpu->store(raw_ct); - - // Check if CPU ciphertext has enough levels to hold GPU ciphertext. - size_t cpu_levels = ct_cpu->GetElements()[0].GetAllElements().size(); - size_t gpu_levels = raw_ct.numRes; - - if (cpu_levels < gpu_levels) { - // Create a fresh ciphertext at the top level with enough space - std::vector dummy(1, 0.0); - auto pt_dummy = context->MakeCKKSPackedPlaintext(dummy, 1, this->multiplicative_depth - ct_gpu->getLevel()); - auto& skImpl = std::any_cast&>(sk->pimpl); - ct_cpu = context->Encrypt(skImpl, pt_dummy); - } - - // Overwrite cpu ct with the data from GPU. - FIDESlib::CKKS::GetOpenFHECipherText(ct_cpu, raw_ct); - } - - auto& skImpl = std::any_cast&>(sk->pimpl); + auto& context = std::any_cast&>(this->host); + auto& ct_host = std::any_cast&>(ct->host); + auto& skImpl = std::any_cast&>(sk->pimpl); lbcrypto::Plaintext ptImpl; - auto res = context->Decrypt(skImpl, ct_cpu, &ptImpl); + auto res = context->Decrypt(skImpl, ct_host, &ptImpl); if (pt->get() != nullptr) { - - if ((*pt)->loaded && !this->devices.empty()) { - OPENFHE_THROW("Inconsistent state: Plaintext is marked as loaded but no devices are available"); - } - if ((*pt)->loaded && !this->EvictDevicePlaintext((*pt)->gpu)) { - OPENFHE_THROW("Plaintext eviction error: could not evict Plaintext from device"); - } - - (*pt)->cpu = std::make_any(std::move(ptImpl)); - (*pt)->loaded = false; - (*pt)->gpu = 0; + (*pt)->host = std::make_any(std::move(ptImpl)); + // Drop any stale backend payload a reused output plaintext may still hold (empty `any` == + // not resident; RAII frees a device copy). Clearing the value type's own slot — no backend + // dispatch, and no inspection of device-residency state in this neutral facade. + (*pt)->device.reset(); } else { - *pt = std::make_shared(); - (*pt)->cpu = std::make_any(std::move(ptImpl)); - (*pt)->loaded = false; - (*pt)->gpu = 0; + *pt = std::make_shared(); + (*pt)->host = std::make_any(std::move(ptImpl)); } DecryptResult result{}; @@ -625,1188 +407,268 @@ DecryptResult CryptoContextImpl::Decrypt(Ciphertext& ct, con } DecryptResult CryptoContextImpl::Decrypt(const PrivateKey& sk, Ciphertext& ct, Plaintext* pt) { - FIDESlib::CudaNvtxRange r("API"); return Decrypt(ct, sk, pt); } +void CryptoContextImpl::RecoverHostCiphertext(Ciphertext& ct) { + engine_->recoverHostCiphertext(*this, ct); +} + // ---- Operations ---- Ciphertext CryptoContextImpl::EvalNegate(const Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto ct = context->EvalNegate(ctImpl); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - res_gpu->multScalar(-1.0); - return result; + return engine_->evalNegate(*this, ct); } void CryptoContextImpl::EvalNegateInPlace(Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct); - auto& ctImpl = std::any_cast&>(ct->cpu); - context->EvalNegateInPlace(ctImpl); - return; - } - - // GPU path. - this->LoadCiphertext(ct); - - auto ct_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - ct_gpu->multScalar(-1.0); + engine_->evalNegateInPlace(*this, ct); } Ciphertext CryptoContextImpl::EvalAdd(const Ciphertext& ct1, const Ciphertext& ct2) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - auto& ct2Impl = std::any_cast&>(ct2->cpu); - auto ct = context->EvalAdd(ct1Impl, ct2Impl); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct1)); - this->LoadCiphertext(const_cast&>(ct2)); - - Ciphertext result = std::make_shared>(*ct1); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - auto ct2_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct2->gpu)); - res_gpu->add(*ct2_gpu); - - return result; + return engine_->evalAdd(*this, ct1, ct2); } Ciphertext CryptoContextImpl::EvalAdd(const Ciphertext& ct, Plaintext& pt) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto& ptImpl = std::any_cast(pt->cpu); - auto ct = context->EvalAdd(ctImpl, ptImpl); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - this->LoadPlaintext(pt); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - auto pt_gpu = std::static_pointer_cast(this->GetDevicePlaintext(pt->gpu)); - res_gpu->addPt(*pt_gpu); - - return result; + return engine_->evalAdd(*this, ct, pt); } Ciphertext CryptoContextImpl::EvalAdd(Plaintext& pt, const Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); return EvalAdd(ct, pt); } Ciphertext CryptoContextImpl::EvalAdd(const Ciphertext& ct, double scalar) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto ct = context->EvalAdd(ctImpl, scalar); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - res_gpu->addScalar(scalar); - - return result; + return engine_->evalAdd(*this, ct, scalar); } Ciphertext CryptoContextImpl::EvalAdd(double scalar, const Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); return EvalAdd(ct, scalar); } void CryptoContextImpl::EvalAddInPlace(Ciphertext& ct1, const Ciphertext& ct2) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct1); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - auto& ct2Impl = std::any_cast&>(ct2->cpu); - context->EvalAddInPlace(ct1Impl, ct2Impl); - return; - } - - // GPU path. - this->LoadCiphertext(ct1); - this->LoadCiphertext(const_cast&>(ct2)); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct1->gpu)); - auto ct2_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct2->gpu)); - res_gpu->add(*ct2_gpu); + engine_->evalAddInPlace(*this, ct1, ct2); } void CryptoContextImpl::EvalAddInPlace(Ciphertext& ct1, Plaintext& pt) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct1); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - auto& ptImpl = std::any_cast(pt->cpu); - context->EvalAddInPlace(ct1Impl, ptImpl); - return; - } - - // GPU path. - this->LoadCiphertext(ct1); - this->LoadPlaintext(pt); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct1->gpu)); - auto pt_gpu = std::static_pointer_cast(this->GetDevicePlaintext(pt->gpu)); - res_gpu->addPt(*pt_gpu); + engine_->evalAddInPlace(*this, ct1, pt); } void CryptoContextImpl::EvalAddInPlace(Plaintext& pt, Ciphertext& ct1) { - FIDESlib::CudaNvtxRange r("API"); EvalAddInPlace(ct1, pt); } void CryptoContextImpl::EvalAddInPlace(Ciphertext& ct1, double scalar) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct1); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - context->EvalAddInPlace(ct1Impl, scalar); - return; - } - - // GPU path. - this->LoadCiphertext(ct1); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct1->gpu)); - res_gpu->addScalar(scalar); + engine_->evalAddInPlace(*this, ct1, scalar); } void CryptoContextImpl::EvalAddInPlace(double scalar, Ciphertext& ct1) { - FIDESlib::CudaNvtxRange r("API"); EvalAddInPlace(ct1, scalar); } Ciphertext CryptoContextImpl::EvalAddMutable(Ciphertext& ct1, Ciphertext& ct2) { - FIDESlib::CudaNvtxRange r("API"); return EvalAdd(ct1, ct2); } Ciphertext CryptoContextImpl::EvalAddMutable(Ciphertext& ct, Plaintext& pt) { - FIDESlib::CudaNvtxRange r("API"); return EvalAdd(ct, pt); } Ciphertext CryptoContextImpl::EvalAddMutable(Plaintext& pt, Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); return EvalAdd(ct, pt); } void CryptoContextImpl::EvalAddMutableInPlace(Ciphertext& ct1, Ciphertext& ct2) { - FIDESlib::CudaNvtxRange r("API"); EvalAddInPlace(ct1, ct2); } Ciphertext CryptoContextImpl::EvalAddMany(const std::vector>& ciphertexts) { - FIDESlib::CudaNvtxRange r("API"); - - if (ciphertexts.empty()) { - OPENFHE_THROW("EvalAddMany: input ciphertext vector is empty"); - } - - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - std::vector> ctImpls; - ctImpls.reserve(ciphertexts.size()); - for (const auto& ct : ciphertexts) { - ctImpls.push_back(std::any_cast&>(ct->cpu)); - } - auto ct = context->EvalAddMany(ctImpls); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - - for (const auto& ct : ciphertexts) { - this->LoadCiphertext(const_cast&>(ct)); - } - - // Initialize result with the first ciphertext. - Ciphertext result = std::make_shared>(*ciphertexts[0]); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - - const size_t inSize = ciphertexts.size(); - const size_t lim = inSize * 2 - 2; - std::vector> ciphertextSumVec; - ciphertextSumVec.resize(inSize - 1); - size_t ctrIndex = 0; - - for (size_t i = 0; i < lim; i = i + 2) { - ciphertextSumVec[ctrIndex++] = - this->EvalAdd(i < inSize ? ciphertexts[i] : ciphertextSumVec[i - inSize], i + 1 < inSize ? ciphertexts[i + 1] : ciphertextSumVec[i + 1 - inSize]); - } - - return ciphertextSumVec.back(); + return engine_->evalAddMany(*this, ciphertexts); } void CryptoContextImpl::EvalAddManyInPlace(std::vector>& ciphertexts) { - FIDESlib::CudaNvtxRange r("API"); - - if (ciphertexts.empty()) { - OPENFHE_THROW("EvalAddManyInPlace: input ciphertext vector is empty"); - } - - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - std::vector> ctImpls; - ctImpls.reserve(ciphertexts.size()); - for (const auto& ct : ciphertexts) { - ctImpls.push_back(std::any_cast&>(ct->cpu)); - } - context->EvalAddManyInPlace(ctImpls); - ciphertexts[0]->cpu = std::make_any>(ctImpls[0]); - return; - } - - // GPU path. - - for (const auto& ct : ciphertexts) { - this->LoadCiphertext(const_cast&>(ct)); - } - - for (size_t j = 1; j < ciphertexts.size(); j = j * 2) { - for (size_t i = 0; i < ciphertexts.size(); i = i + 2 * j) { - if ((i + j) < ciphertexts.size()) { - if (ciphertexts[i] != nullptr && ciphertexts[i + j] != nullptr) { - this->EvalAddInPlace(ciphertexts[i], ciphertexts[i + j]); - } else if (ciphertexts[i] == nullptr && ciphertexts[i + j] != nullptr) { - ciphertexts[i] = ciphertexts[i + j]; - } - } - } - } + engine_->evalAddManyInPlace(*this, ciphertexts); } Ciphertext CryptoContextImpl::EvalSub(const Ciphertext& ct1, const Ciphertext& ct2) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - auto& ct2Impl = std::any_cast&>(ct2->cpu); - auto ct = context->EvalSub(ct1Impl, ct2Impl); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct1)); - this->LoadCiphertext(const_cast&>(ct2)); - - Ciphertext result = std::make_shared>(*ct1); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - auto ct2_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct2->gpu)); - res_gpu->sub(*ct2_gpu); - - return result; + return engine_->evalSub(*this, ct1, ct2); } Ciphertext CryptoContextImpl::EvalSub(const Ciphertext& ct, Plaintext& pt) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto& ptImpl = std::any_cast(pt->cpu); - auto ct = context->EvalSub(ctImpl, ptImpl); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - this->LoadPlaintext(pt); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - auto pt_gpu = std::static_pointer_cast(this->GetDevicePlaintext(pt->gpu)); - res_gpu->subPt(*pt_gpu); - - return result; + return engine_->evalSub(*this, ct, pt); } Ciphertext CryptoContextImpl::EvalSub(Plaintext& pt, const Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto& ptImpl = std::any_cast(pt->cpu); - auto ct = context->EvalSub(ptImpl, ctImpl); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - this->LoadPlaintext(pt); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - auto pt_gpu = std::static_pointer_cast(this->GetDevicePlaintext(pt->gpu)); - res_gpu->multScalar(-1.0); - res_gpu->addPt(*pt_gpu); - - return result; + return engine_->evalSub(*this, pt, ct); } Ciphertext CryptoContextImpl::EvalSub(const Ciphertext& ct, double scalar) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto ct = context->EvalSub(ctImpl, scalar); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - res_gpu->addScalar(-scalar); - - return result; + return engine_->evalSub(*this, ct, scalar); } Ciphertext CryptoContextImpl::EvalSub(double scalar, const Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto ct = context->EvalSub(scalar, ctImpl); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - res_gpu->multScalar(-1.0); - res_gpu->addScalar(scalar); - res_gpu->multScalar(-1.0); - - return result; + return engine_->evalSub(*this, scalar, ct); } void CryptoContextImpl::EvalSubInPlace(Ciphertext& ct1, const Ciphertext& ct2) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct1); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - auto& ct2Impl = std::any_cast&>(ct2->cpu); - context->EvalSubInPlace(ct1Impl, ct2Impl); - return; - } - - // GPU path. - this->LoadCiphertext(ct1); - this->LoadCiphertext(const_cast&>(ct2)); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct1->gpu)); - auto ct2_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct2->gpu)); - res_gpu->sub(*ct2_gpu); + engine_->evalSubInPlace(*this, ct1, ct2); } void CryptoContextImpl::EvalSubInPlace(Ciphertext& ct1, double scalar) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct1); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - context->EvalSubInPlace(ct1Impl, scalar); - return; - } - - // GPU path. - this->LoadCiphertext(ct1); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct1->gpu)); - res_gpu->addScalar(-scalar); + engine_->evalSubInPlace(*this, ct1, scalar); } void CryptoContextImpl::EvalSubInPlace(double scalar, Ciphertext& ct1) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct1); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - context->EvalSubInPlace(scalar, ct1Impl); - return; - } - - // GPU path. - this->LoadCiphertext(ct1); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct1->gpu)); - res_gpu->multScalar(-1.0); - res_gpu->addScalar(scalar); - res_gpu->multScalar(-1.0); + engine_->evalSubInPlace(*this, scalar, ct1); } Ciphertext CryptoContextImpl::EvalSubMutable(Ciphertext& ct1, Ciphertext& ct2) { - FIDESlib::CudaNvtxRange r("API"); return EvalSub(ct1, ct2); } Ciphertext CryptoContextImpl::EvalSubMutable(Ciphertext& ct, Plaintext& pt) { - FIDESlib::CudaNvtxRange r("API"); return EvalSub(ct, pt); } Ciphertext CryptoContextImpl::EvalSubMutable(Plaintext& pt, Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); return EvalSub(pt, ct); } void CryptoContextImpl::EvalSubMutableInPlace(Ciphertext& ct1, Ciphertext& ct2) { - FIDESlib::CudaNvtxRange r("API"); EvalSubInPlace(ct1, ct2); } Ciphertext CryptoContextImpl::EvalMult(const Ciphertext& ct1, const Ciphertext& ct2) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - auto& ct2Impl = std::any_cast&>(ct2->cpu); - auto ct = context->EvalMult(ct1Impl, ct2Impl); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct1)); - this->LoadCiphertext(const_cast&>(ct2)); - - Ciphertext result = std::make_shared>(*ct1); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - auto ct2_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct2->gpu)); - res_gpu->mult(*ct2_gpu); - - return result; + return engine_->evalMult(*this, ct1, ct2); } Ciphertext CryptoContextImpl::EvalMult(const Ciphertext& ct1, Plaintext& pt) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - auto& ptImpl = std::any_cast(pt->cpu); - auto ct = context->EvalMult(ct1Impl, ptImpl); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct1)); - this->LoadPlaintext(pt); - - Ciphertext result = std::make_shared>(*ct1); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - auto pt_gpu = std::static_pointer_cast(this->GetDevicePlaintext(pt->gpu)); - res_gpu->multPt(*pt_gpu); - - return result; + return engine_->evalMult(*this, ct1, pt); } Ciphertext CryptoContextImpl::EvalMult(Plaintext& pt, const Ciphertext& ct1) { - FIDESlib::CudaNvtxRange r("API"); return EvalMult(ct1, pt); } Ciphertext CryptoContextImpl::EvalMult(const Ciphertext& ct1, double scalar) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - auto ct = context->EvalMult(ct1Impl, scalar); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct1)); - - Ciphertext result = std::make_shared>(*ct1); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - res_gpu->multScalar(scalar); - - return result; + return engine_->evalMult(*this, ct1, scalar); } Ciphertext CryptoContextImpl::EvalMult(double scalar, const Ciphertext& ct1) { - FIDESlib::CudaNvtxRange r("API"); return EvalMult(ct1, scalar); } void CryptoContextImpl::EvalMultInPlace(Ciphertext& ct1, Plaintext& pt) { - FIDESlib::CudaNvtxRange r("API"); - - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct1); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - auto& ptImpl = std::any_cast(pt->cpu); - auto res = context->EvalMult(ct1Impl, ptImpl); - ct1->cpu = std::make_any>(res); - return; - } - - // GPU path. - this->LoadCiphertext(ct1); - this->LoadPlaintext(pt); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct1->gpu)); - auto pt_gpu = std::static_pointer_cast(this->GetDevicePlaintext(pt->gpu)); - res_gpu->multPt(*pt_gpu); + engine_->evalMultInPlace(*this, ct1, pt); } void CryptoContextImpl::EvalMultInPlace(Ciphertext& ct1, double scalar) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct1); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - context->EvalMultInPlace(ct1Impl, scalar); - return; - } - - // GPU path. - this->LoadCiphertext(ct1); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct1->gpu)); - res_gpu->multScalar(scalar); + engine_->evalMultInPlace(*this, ct1, scalar); } void CryptoContextImpl::EvalMultInPlace(double scalar, Ciphertext& ct1) { - FIDESlib::CudaNvtxRange r("API"); EvalMultInPlace(ct1, scalar); } Ciphertext CryptoContextImpl::EvalMultMutable(Ciphertext& ct1, Ciphertext& ct2) { - FIDESlib::CudaNvtxRange r("API"); return EvalMult(ct1, ct2); } Ciphertext CryptoContextImpl::EvalMultMutable(Ciphertext& ct, Plaintext& pt) { - FIDESlib::CudaNvtxRange r("API"); return EvalMult(ct, pt); } Ciphertext CryptoContextImpl::EvalMultMutable(Plaintext& pt, Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); return EvalMult(ct, pt); } void CryptoContextImpl::EvalMultMutableInPlace(Ciphertext& ct1, Ciphertext& ct2) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct1); - EnsureMutableCpuCiphertext(ct2); - auto& ct1Impl = std::any_cast&>(ct1->cpu); - auto& ct2Impl = std::any_cast&>(ct2->cpu); - context->EvalMultMutableInPlace(ct1Impl, ct2Impl); - return; - } - - this->LoadCiphertext(ct1); - this->LoadCiphertext(ct2); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct1->gpu)); - auto ct2_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct2->gpu)); - res_gpu->mult(*ct2_gpu); + engine_->evalMultInPlace(*this, ct1, ct2); } Ciphertext CryptoContextImpl::EvalSquare(const Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto ct = context->EvalSquare(ctImpl); - Ciphertext ciphertext = std::make_shared>(this->self_reference.lock()); - ciphertext->cpu = std::make_any>(ct); - return ciphertext; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - res_gpu->square(); - - return result; + return engine_->evalSquare(*this, ct); } void CryptoContextImpl::EvalSquareInPlace(Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct); - auto& ctImpl = std::any_cast&>(ct->cpu); - context->EvalSquareInPlace(ctImpl); - return; - } - - // GPU path. - this->LoadCiphertext(ct); - - auto ct_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - ct_gpu->square(); + engine_->evalSquareInPlace(*this, ct); } Ciphertext CryptoContextImpl::EvalSquareMutable(Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); return EvalSquare(ct); } Ciphertext CryptoContextImpl::EvalRotate(const Ciphertext& ciphertext, int32_t index) { - FIDESlib::CudaNvtxRange r("API"); - - { - FIDESlib::CudaNvtxRange r("API_fallback"); - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ciphertext->cpu); - auto ct = context->EvalRotate(ctImpl, index); - Ciphertext result = std::make_shared>(this->self_reference.lock()); - result->cpu = std::make_any>(ct); - return result; - } - } - // GPU path. - this->LoadCiphertext(const_cast&>(ciphertext)); - - Ciphertext result = std::make_shared>(*ciphertext); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - res_gpu->rotate(index); - - return result; + return engine_->evalRotate(*this, ciphertext, index); } void CryptoContextImpl::EvalRotateInPlace(Ciphertext& ciphertext, int32_t index) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ciphertext->cpu); - auto ct = context->EvalRotate(ctImpl, index); - ciphertext->cpu = std::make_any>(ct); - return; - } - - // GPU path. - this->LoadCiphertext(ciphertext); - - auto ct_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ciphertext->gpu)); - ct_gpu->rotate(index); + engine_->evalRotateInPlace(*this, ciphertext, index); } std::shared_ptr CryptoContextImpl::EvalFastRotationPrecompute(const Ciphertext& ct) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - return context->EvalFastRotationPrecompute(ctImpl); - } - - // GPU path not needed. - - return nullptr; + return engine_->evalFastRotationPrecompute(*this, ct); } -#include - Ciphertext CryptoContextImpl::EvalFastRotation(const Ciphertext& ct, const int32_t index, const uint32_t m, const std::shared_ptr& precomp) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto casted = std::static_pointer_cast>>>>(precomp); - Ciphertext result = std::make_shared>(this->self_reference.lock()); - result->cpu = std::make_any>(context->EvalFastRotation(ctImpl, index, m, casted)); - return result; - } - - // GPU path. Inefficient for only one rotation. - - this->LoadCiphertext(const_cast&>(ct)); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - auto ct_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - res_gpu->copy(*ct_gpu); - res_gpu->rotate((int)index, true); - - return result; + return engine_->evalFastRotation(*this, ct, index, m, precomp); } Ciphertext CryptoContextImpl::EvalFastRotationExt(const Ciphertext& ct, const int32_t index, const std::shared_ptr& digits, bool addFirst) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto casted = std::static_pointer_cast>>>>(digits); - Ciphertext result = std::make_shared>(this->self_reference.lock()); - result->cpu = std::make_any>(context->EvalFastRotationExt(ctImpl, index, casted, addFirst)); - return result; - } - - // GPU path. Inefficient for only one rotation. - - this->LoadCiphertext(const_cast&>(ct)); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - auto ct_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - // ct_gpu->rotate((int)index, false); - res_gpu->copy(*ct_gpu); - res_gpu->rotate((int)index, false); - - return result; + return engine_->evalFastRotationExt(*this, ct, index, digits, addFirst); } std::vector> CryptoContextImpl::EvalFastRotation(const Ciphertext& ct, const std::vector& indices, const uint32_t m, const std::shared_ptr& precomp) { - FIDESlib::CudaNvtxRange r("API"); - - std::vector> results; - - // Fall back to CPU. - if (this->devices.empty()) { - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto casted = std::static_pointer_cast>>>>(precomp); - - for (const auto& index : indices) { - Ciphertext result = std::make_shared>(*ct); - result->cpu = std::make_any>(context->EvalFastRotation(ctImpl, index, m, casted)); - results.push_back(result); - } - return results; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - - auto ct_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - - // Create result ciphertexts. - std::vector results_gpu; - std::vector indices_real; - for (int indice : indices) { - Ciphertext result = std::make_shared>(*ct); - - if (indice != 0) { - indices_real.push_back(indice); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - results_gpu.push_back(res_gpu.get()); - } - results.push_back(result); - } - - ct_gpu->rotate_hoisted(indices_real, results_gpu, false); - return results; + return engine_->evalFastRotation(*this, ct, indices, m, precomp); } std::vector> CryptoContextImpl::EvalFastRotationExt(const Ciphertext& ct, const std::vector& indices, const std::shared_ptr& digits, bool addFirst) { - FIDESlib::CudaNvtxRange r("API"); - - std::vector> results; - - // Fall back to CPU. - if (this->devices.empty()) { - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto casted = std::static_pointer_cast>>>>(digits); - - for (const auto& index : indices) { - Ciphertext result = std::make_shared>(*ct); - result->cpu = std::make_any>(context->EvalFastRotationExt(ctImpl, index, casted, addFirst)); - results.push_back(result); - } - return results; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - - auto ct_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - - std::vector results_gpu; - std::vector indices_real; - for (int indice : indices) { - Ciphertext result = std::make_shared>(*ct); - - if (indice != 0) { - indices_real.push_back(indice); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - results_gpu.push_back(res_gpu.get()); - } - results.push_back(result); - } - - ct_gpu->rotate_hoisted(indices_real, results_gpu, true); - return results; + return engine_->evalFastRotationExt(*this, ct, indices, digits, addFirst); } Ciphertext CryptoContextImpl::EvalChebyshevSeries(const Ciphertext& ct, std::vector& coeffs, double a, double b) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto ct = context->EvalChebyshevSeries(ctImpl, coeffs, a, b); - Ciphertext result = std::make_shared>(this->self_reference.lock()); - result->cpu = std::make_any>(ct); - return result; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - FIDESlib::CKKS::evalChebyshevSeries(*res_gpu, coeffs, a, b); - - return result; + return engine_->evalChebyshevSeries(*this, ct, coeffs, a, b); } void CryptoContextImpl::EvalChebyshevSeriesInPlace(Ciphertext& ct, std::vector& coeffs, double a, double b) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct); - auto& ctImpl = std::any_cast&>(ct->cpu); - auto res = context->EvalChebyshevSeries(ctImpl, coeffs, a, b); - ct->cpu = std::make_any>(res); - return; - } - - // GPU path. - this->LoadCiphertext(ct); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - FIDESlib::CKKS::evalChebyshevSeries(*res_gpu, coeffs, a, b); + engine_->evalChebyshevSeriesInPlace(*this, ct, coeffs, a, b); } std::vector CryptoContextImpl::GetChebyshevCoefficients(std::function& func, double a, double b, size_t degree) { - FIDESlib::CudaNvtxRange r("API"); - return FIDESlib::CKKS::get_chebyshev_coefficients(func, a, b, degree); + return fideslib::get_chebyshev_coefficients(func, a, b, degree); } Ciphertext CryptoContextImpl::Rescale(const Ciphertext& ciphertext) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ciphertext->cpu); - auto ct = context->Rescale(ctImpl); - Ciphertext result = std::make_shared>(this->self_reference.lock()); - result->cpu = std::make_any>(ct); - return result; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ciphertext)); - - Ciphertext result = std::make_shared>(*ciphertext); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - res_gpu->rescale(); - - return result; + return engine_->rescale(*this, ciphertext); } void CryptoContextImpl::RescaleInPlace(Ciphertext& ciphertext) { - FIDESlib::CudaNvtxRange r("API"); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ciphertext); - auto& ctImpl = std::any_cast&>(ciphertext->cpu); - auto ct = context->Rescale(ctImpl); - ciphertext->cpu = std::make_any>(ct); - return; - } - - // GPU path. - this->LoadCiphertext(ciphertext); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ciphertext->gpu)); - res_gpu->rescale(); + engine_->rescaleInPlace(*this, ciphertext); } void CryptoContextImpl::SetLevel(Ciphertext& ct, size_t level) { - FIDESlib::CudaNvtxRange r("API"); ct->SetLevel(level); } Ciphertext CryptoContextImpl::EvalBootstrap(const Ciphertext& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled) { - FIDESlib::CudaNvtxRange r("API"); - - auto& context = std::any_cast&>(this->cpu); - // Fall back to CPU. - if (this->devices.empty()) { - - auto& ctImpl = std::any_cast&>(ciphertext->cpu); - auto ct = context->EvalBootstrap(ctImpl, numIterations, precision); - Ciphertext result = std::make_shared>(this->self_reference.lock()); - result->cpu = std::make_any>(ct); - return result; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ciphertext)); - - Ciphertext result = std::make_shared>(*ciphertext); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - - FIDESlib::CKKS::Bootstrap(*res_gpu, res_gpu->slots, prescaled); - - return result; + return engine_->evalBootstrap(*this, ciphertext, numIterations, precision, prescaled); } void CryptoContextImpl::EvalBootstrapInPlace(Ciphertext& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled) { - FIDESlib::CudaNvtxRange r("API"); - auto& context = std::any_cast&>(this->cpu); - - // Fall back to CPU. - if (this->devices.empty()) { - - auto& ctImpl = std::any_cast&>(ciphertext->cpu); - auto ct = context->EvalBootstrap(ctImpl, numIterations, precision); - Ciphertext result = std::make_shared>(this->self_reference.lock()); - result->cpu = std::make_any>(ct); - ciphertext = result; - return; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ciphertext)); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ciphertext->gpu)); - - FIDESlib::CKKS::Bootstrap(*res_gpu, res_gpu->slots, prescaled); + engine_->evalBootstrapInPlace(*this, ciphertext, numIterations, precision, prescaled); } Ciphertext CryptoContextImpl::AccumulateSum(const Ciphertext& ct, int slots, int stride) { - FIDESlib::CudaNvtxRange r("API"); - - if (this->devices.empty()) { - auto& context = std::any_cast&>(this->cpu); - auto& ctImpl = std::any_cast&>(ct->cpu); - - lbcrypto::Ciphertext result_ct = std::make_shared>(ctImpl); - - for (int i = 0; i < log2(slots); i++) { - int rot_idx = stride * (1 << i); - auto tmp = context->EvalRotate(result_ct, rot_idx); - context->EvalAddInPlace(result_ct, tmp); - } - - Ciphertext result = std::make_shared>(this->self_reference.lock()); - result->cpu = std::make_any>(result_ct); - return result; - } - - // GPU path. - this->LoadCiphertext(const_cast&>(ct)); - - Ciphertext result = std::make_shared>(*ct); - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(result->gpu)); - - FIDESlib::CKKS::Accumulate(*res_gpu, 4, stride, slots); - - return result; + return engine_->accumulateSum(*this, ct, slots, stride); } void CryptoContextImpl::AccumulateSumInPlace(Ciphertext& ct, int slots, int stride) { - FIDESlib::CudaNvtxRange r("API"); - - if (this->devices.empty()) { - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct); - auto& ctImpl = std::any_cast&>(ct->cpu); - - for (int i = 0; i < log2(slots); i++) { - int rot_idx = stride * (1 << i); - auto tmp = context->EvalRotate(ctImpl, rot_idx); - context->EvalAddInPlace(ctImpl, tmp); - } - - return; - } - - // GPU path. - this->LoadCiphertext(ct); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - - FIDESlib::CKKS::Accumulate(*res_gpu, 4, stride, slots); + engine_->accumulateSumInPlace(*this, ct, slots, stride); } void CryptoContextImpl::AccumulateSumInPlace(Ciphertext& ct, int slots, int stride, int start) { - - if (this->devices.empty()) { - auto& context = std::any_cast&>(this->cpu); - EnsureMutableCpuCiphertext(ct); - auto& ctImpl = std::any_cast&>(ct->cpu); - - for (int s = start; s < slots; s <<= 1) { - int rot_idx = stride * s; - auto tmp = context->EvalRotate(ctImpl, rot_idx); - context->EvalAddInPlace(ctImpl, tmp); - } - - return; - } - - // GPU path. - this->LoadCiphertext(ct); - - auto res_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - - FIDESlib::CKKS::Accumulate(*res_gpu, 4, stride, slots, start); + engine_->accumulateSumInPlace(*this, ct, slots, stride, start); } void CryptoContextImpl::ConvolutionTransformInPlace(Ciphertext& ct, @@ -1816,28 +678,7 @@ void CryptoContextImpl::ConvolutionTransformInPlace(Ciphertext& indexes, int stride, int rowSize) { - FIDESlib::CudaNvtxRange r("API"); - - if (this->devices.empty()) { - OPENFHE_THROW("Not implemented for CPU path"); - } - - // GPU path. - this->LoadCiphertext(ct); - auto ct_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - std::vector pts_gpu; - pts_gpu.reserve(pts.size()); - for (const auto& pt : pts) { - this->LoadPlaintext(const_cast(pt)); - auto pt_gpu = std::static_pointer_cast(this->GetDevicePlaintext(pt->gpu)); - pts_gpu.push_back(pt_gpu.get()); - } - - if (rowSize == 0) { - rowSize = bStep * gStep; - } - - FIDESlib::CKKS::ConvolutionTransform(*ct_gpu, rowSize, bStep, pts_gpu, stride, indexes, gStep); + engine_->convolutionTransformInPlace(*this, ct, gStep, bStep, pts, indexes, stride, rowSize); } void CryptoContextImpl::SpecialConvolutionTransformInPlace(Ciphertext& ct, @@ -1849,123 +690,37 @@ void CryptoContextImpl::SpecialConvolutionTransformInPlace(Ciphertext< int stride, int maskRotationStride, int rowSize) { - FIDESlib::CudaNvtxRange r("API"); - - if (this->devices.empty()) { - OPENFHE_THROW("Not implemented for CPU path"); - } - - // GPU path. - this->LoadCiphertext(ct); - auto ct_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct->gpu)); - std::vector pts_gpu; - pts_gpu.reserve(pts.size()); - for (const auto& pt : pts) { - this->LoadPlaintext(const_cast(pt)); - auto pt_gpu = std::static_pointer_cast(this->GetDevicePlaintext(pt->gpu)); - pts_gpu.push_back(pt_gpu.get()); - } - - // Load mask - this->LoadPlaintext(mask); - auto mask_gpu = std::static_pointer_cast(this->GetDevicePlaintext(mask->gpu)); - - if (rowSize == 0) { - rowSize = bStep * gStep; - } - - FIDESlib::CKKS::SpecialConvolutionTransform(*ct_gpu, rowSize, bStep, pts_gpu, *mask_gpu, stride, maskRotationStride, indexes, gStep); + engine_->specialConvolutionTransformInPlace(*this, ct, gStep, bStep, pts, mask, indexes, stride, maskRotationStride, rowSize); } -// ---- Copy helpers ---- - -uint32_t CryptoContextImpl::CopyDeviceCiphertext(const CiphertextImpl& ct) { - FIDESlib::CudaNvtxRange r("API"); - if (!ct.loaded) { - OPENFHE_THROW("Ciphertext not loaded to any device"); - } +// ---- Ciphertext backend hooks ---- - auto& context_gpu = std::any_cast(this->gpu); - auto ct_gpu = std::static_pointer_cast(this->GetDeviceCiphertext(ct.gpu)); - auto new_ct = std::make_shared(context_gpu); - new_ct->copy(*ct_gpu); - uint32_t handle = this->RegisterDeviceCiphertext(std::move(new_ct)); - return handle; +std::any CryptoContextImpl::CloneCiphertextBackend(const CiphertextImpl& src) { + return engine_->cloneCiphertextBackend(*this, src); } -// ---- Map Handling ---- +size_t CryptoContextImpl::CiphertextLevel(const CiphertextImpl& ct) { + return engine_->ciphertextLevel(*this, ct); +} -uint32_t CryptoContextImpl::RegisterDevicePlaintext(std::shared_ptr&& p) { - FIDESlib::CudaNvtxRange r("API"); - if (this->devices.empty()) { - OPENFHE_THROW("No devices available to register plaintext"); - } - // device_plaintexts_mutex->lock(); - uint32_t handle = next_gpu_handle++; - device_plaintexts.emplace(handle, std::move(p)); - // device_plaintexts_mutex->unlock(); - return handle; +size_t CryptoContextImpl::CiphertextNoiseScaleDeg(const CiphertextImpl& ct) { + return engine_->ciphertextNoiseScaleDeg(*this, ct); } -uint32_t CryptoContextImpl::RegisterDeviceCiphertext(std::shared_ptr&& c) { - FIDESlib::CudaNvtxRange r("API"); - if (this->devices.empty()) { - OPENFHE_THROW("No devices available to register ciphertext"); - } - // device_ciphertexts_mutex->lock(); - uint32_t handle = next_gpu_handle++; - device_ciphertexts.emplace(handle, std::move(c)); - // device_ciphertexts_mutex->unlock(); - return handle; -} - -std::shared_ptr& CryptoContextImpl::GetDevicePlaintext(uint32_t handle) { - FIDESlib::CudaNvtxRange r("API"); - // device_plaintexts_mutex->lock_shared(); - auto& it = device_plaintexts.at(handle); - // device_plaintexts_mutex->unlock_shared(); - return it; -} - -std::shared_ptr& CryptoContextImpl::GetDeviceCiphertext(uint32_t handle) { - FIDESlib::CudaNvtxRange r("API"); - // device_ciphertexts_mutex->lock_shared(); - auto& it = device_ciphertexts.at(handle); - // device_ciphertexts_mutex->unlock_shared(); - return it; -} - -bool CryptoContextImpl::EvictDevicePlaintext(uint32_t handle) { - FIDESlib::CudaNvtxRange r("API"); - // device_plaintexts_mutex->lock(); - auto result = device_plaintexts.erase(handle) > 0; - // device_plaintexts_mutex->unlock(); - return result; +void CryptoContextImpl::SetCiphertextSlots(CiphertextImpl& ct, size_t slots) { + engine_->setCiphertextSlots(*this, ct, slots); } -bool CryptoContextImpl::EvictDeviceCiphertext(uint32_t handle) { - FIDESlib::CudaNvtxRange r("API"); - // device_ciphertexts_mutex->lock(); - auto result = device_ciphertexts.erase(handle) > 0; - // device_ciphertexts_mutex->unlock(); - return result; +void CryptoContextImpl::SetCiphertextLevel(CiphertextImpl& ct, size_t level) { + engine_->setCiphertextLevel(*this, ct, level); } void CryptoContextImpl::Synchronize() const { - FIDESlib::CudaNvtxRange r("API"); - if (this->devices.empty() || !this->loaded) { - return; - } - for (const auto& device : this->devices) { - cudaSetDevice(device); - cudaDeviceSynchronize(); - CudaCheckErrorModNoSync; - } + engine_->synchronize(); } std::vector CryptoContextImpl::GetConvolutionTransformRotationIndices(int rowSize, int bStep, int stride, uint32_t gStep) { - FIDESlib::CudaNvtxRange r("API"); - return FIDESlib::CKKS::GetConvolutionTransformRotationIndices(rowSize, bStep, stride, gStep); + return fideslib::GetConvolutionTransformRotationIndices(rowSize, bStep, stride, gStep); } } // namespace fideslib \ No newline at end of file diff --git a/api/CryptoContext.hpp b/api/CryptoContext.hpp index 43556f2b..3ffc173e 100644 --- a/api/CryptoContext.hpp +++ b/api/CryptoContext.hpp @@ -20,6 +20,8 @@ namespace fideslib { +class Engine; + /// @brief Specialization of CryptoContext for the DCRTPoly representation. template <> class CryptoContextImpl { @@ -48,11 +50,15 @@ template <> class CryptoContextImpl { uint32_t GetCyclotomicOrder() const; uint32_t GetRingDimension() const; double GetPreScaleFactor(uint32_t slots); + /// @brief Devices this context is configured to use (empty on the CPU backend). + std::vector GetCudaDevices() const; // ---- Setters ---- void SetAutoLoadPlaintexts(bool autoload); void SetAutoLoadCiphertexts(bool autoload); - void SetDevices(const std::vector& devices); + /// @brief Select the CUDA devices this context loads onto (CUDA backend only). + /// Must be called before LoadContext; forwarded to the engine. + void SetCudaDevices(const std::vector& devices); // ---- Load to devices ---- @@ -109,6 +115,11 @@ template <> class CryptoContextImpl { Ciphertext Encrypt(const PrivateKey& sk, Plaintext& pt); DecryptResult Decrypt(Ciphertext& ct, const PrivateKey& sk, Plaintext* pt); DecryptResult Decrypt(const PrivateKey& sk, Ciphertext& ct, Plaintext* pt); + // Refresh ct's host shadow (ct->host) with the backend's current value, syncing it back from the + // device if resident (without evicting — the device copy stays resident), so the underlying + // lbcrypto::Ciphertext can be recovered without decrypting. Used by the api-vs-OpenFHE parity + // tests; the readback itself is backend-specific (see Engine). + void RecoverHostCiphertext(Ciphertext& ct); // ---- Operations ---- @@ -204,12 +215,10 @@ template <> class CryptoContextImpl { public: // ---- Internal State ---- - std::any cpu; - std::any gpu; - /// @brief Whether the context has been loaded to the devices. - bool loaded = false; - /// @brief List of devices the context is loaded on. - std::vector devices = { 0 }; + /// @brief Canonical host value (lbcrypto::CryptoContext); the backend-neutral context. + std::any host; + /// @brief Backend engine this context dispatches operations to (CPU or CUDA). Owns all backend state. + std::unique_ptr engine_; /// @brief Whether plaintexts should be automatically loaded to the device upon encryption. bool auto_load_plaintexts = false; /// @brief Whether ciphertexts should be automatically loaded to the device upon creation. @@ -225,25 +234,13 @@ template <> class CryptoContextImpl { /// @brief Secret key distribution. SecretKeyDist keyDist = UNIFORM_TERNARY; - // ---- Copy helpers ---- - - uint32_t CopyDeviceCiphertext(const CiphertextImpl& ct); - - // --- Map Handling ---- - - /// @brief Registry of plaintexts stored on the GPU (opaque types). - std::unordered_map> device_plaintexts; - /// @brief Registry of ciphertexts stored on the GPU (opaque types). - std::unordered_map> device_ciphertexts; - /// @brief Next available handle for GPU objects. Zero is reserved as a null handle. - uint32_t next_gpu_handle = 1; - - uint32_t RegisterDevicePlaintext(std::shared_ptr&& p); - uint32_t RegisterDeviceCiphertext(std::shared_ptr&& c); - std::shared_ptr& GetDevicePlaintext(uint32_t handle); - std::shared_ptr& GetDeviceCiphertext(uint32_t handle); - bool EvictDevicePlaintext(uint32_t handle); - bool EvictDeviceCiphertext(uint32_t handle); + // ---- Ciphertext backend hooks ---- + // Thin pass-throughs the value type uses to reach its backend without naming any device type. + std::any CloneCiphertextBackend(const CiphertextImpl& src); + size_t CiphertextLevel(const CiphertextImpl& ct); + size_t CiphertextNoiseScaleDeg(const CiphertextImpl& ct); + void SetCiphertextSlots(CiphertextImpl& ct, size_t slots); + void SetCiphertextLevel(CiphertextImpl& ct, size_t level); void Synchronize() const; diff --git a/api/Definitions.hpp b/api/Definitions.hpp index 7b308a01..c5646490 100644 --- a/api/Definitions.hpp +++ b/api/Definitions.hpp @@ -54,38 +54,38 @@ struct DecryptResult { /// @brief Enumeration of supported scaling techniques. enum ScalingTechnique { - FIXEDMANUAL = 0, - FIXEDAUTO, - FLEXIBLEAUTO, - FLEXIBLEAUTOEXT, + FIXEDMANUAL = 0, + FIXEDAUTO, + FLEXIBLEAUTO, + FLEXIBLEAUTOEXT, }; /// @brief Enumeration of supported key switching techniques. enum KeySwitchTechnique { - INVALID_KS_TECH = 0, - //BV, - HYBRID = 2, + INVALID_KS_TECH = 0, + // BV, + HYBRID = 2, }; /// @brief Enumeration of supported secret key distributions. enum SecretKeyDist { - GAUSSIAN = 0, - UNIFORM_TERNARY = 1, - SPARSE_TERNARY = 2, - SPARSE_ENCAPSULATED = 3, + GAUSSIAN = 0, + UNIFORM_TERNARY = 1, + SPARSE_TERNARY = 2, + SPARSE_ENCAPSULATED = 3, }; /// @brief Enumeration of supported security levels. enum SecurityLevel { - HEStd_128_classic, - HEStd_192_classic, - HEStd_256_classic, - HEStd_128_quantum, - HEStd_192_quantum, - HEStd_256_quantum, - HEStd_NotSet, + HEStd_128_classic, + HEStd_192_classic, + HEStd_256_classic, + HEStd_128_quantum, + HEStd_192_quantum, + HEStd_256_quantum, + HEStd_NotSet, }; } // namespace fideslib -#endif \ No newline at end of file +#endif diff --git a/api/GenCryptoContext.cpp b/api/GenCryptoContext.cpp index 78576888..42f03c6a 100644 --- a/api/GenCryptoContext.cpp +++ b/api/GenCryptoContext.cpp @@ -1,5 +1,8 @@ #include "GenCryptoContext.hpp" +#include "engine/Backend.hpp" +#include "engine/Engine.hpp" + #include #include @@ -8,21 +11,28 @@ namespace fideslib { CryptoContext GenCryptoContext(CCParams& params) { - auto& impl_params = std::any_cast&>(params.cpu); + // CUDA needs a uniform main key, so a sparse-ternary secret key is unsupported there; reject it + // loudly here, now that the backend and key distribution are both final, rather than silently + // downgrading it (which would also desync params from the keyDist the bootstrap path reads). + if (params.backend == Backend::CUDA && params.keyDist == SPARSE_TERNARY) { + OPENFHE_THROW("CUDA backend does not support SPARSE_TERNARY (a sparse main key); " + "use UNIFORM_TERNARY, SPARSE_ENCAPSULATED for sparse bootstrapping, or the CPU backend"); + } + auto& impl_params = std::any_cast&>(params.host); auto cc = lbcrypto::GenCryptoContext(impl_params); if (std::dynamic_pointer_cast(cc->GetScheme()->m_FHE)) std::dynamic_pointer_cast(cc->GetScheme()->m_FHE)->m_bootPrecomMap.clear(); CryptoContextImpl context; - context.cpu = std::make_any>(cc); - context.devices = std::vector(params.devices); - context.auto_load_plaintexts = params.plaintextAutoload; - context.auto_load_ciphertexts = params.ciphertextAutoload; - context.multiplicative_depth = impl_params.GetMultiplicativeDepth(); - context.keyDist = params.keyDist; - auto ptr = std::make_shared>(std::move(context)); - ptr->self_reference = std::weak_ptr>(ptr); + context.host = std::make_any>(cc); + context.engine_ = MakeEngine(params.backend); + context.auto_load_plaintexts = params.plaintextAutoload; + context.auto_load_ciphertexts = params.ciphertextAutoload; + context.multiplicative_depth = impl_params.GetMultiplicativeDepth(); + context.keyDist = params.keyDist; + auto ptr = std::make_shared>(std::move(context)); + ptr->self_reference = std::weak_ptr>(ptr); return ptr; } diff --git a/api/HostMath.cpp b/api/HostMath.cpp new file mode 100644 index 00000000..485b6cff --- /dev/null +++ b/api/HostMath.cpp @@ -0,0 +1,60 @@ +#include "HostMath.hpp" + +#include // OPENFHE_THROW + +#include +#include + +namespace fideslib { + +// Moved verbatim from src/PolyApprox.cu (was in the GPU CKKS namespace). +std::vector get_chebyshev_coefficients(const std::function& func, const double a, const double b, const uint32_t degree) { + if (!degree) { + OPENFHE_THROW("The degree of approximation can not be zero"); + } + + const size_t coeffTotal{ degree + 1 }; + const double bMinusA = 0.5 * (b - a); + const double bPlusA = 0.5 * (b + a); + const double PiByDeg = M_PI / static_cast(coeffTotal); + std::vector functionPoints(coeffTotal); + for (size_t i = 0; i < coeffTotal; ++i) + functionPoints[i] = func(std::cos(PiByDeg * (i + 0.5)) * bMinusA + bPlusA); + + const double multFactor = 2.0 / static_cast(coeffTotal); + std::vector coefficients(coeffTotal); + for (size_t i = 0; i < coeffTotal; ++i) { + for (size_t j = 0; j < coeffTotal; ++j) + coefficients[i] += functionPoints[j] * std::cos(PiByDeg * i * (j + 0.5)); + coefficients[i] *= multFactor; + } + return coefficients; +} + +// Moved verbatim from src/CKKS/LinearTransform.cu (was in the GPU CKKS namespace). +std::vector GetConvolutionTransformRotationIndices(int rowSize, int bStep, int stride, uint32_t gStep) { + std::vector res; + // Internal block size for DotProductPtInternal + constexpr uint32_t INTERNAL_GSTEP = 8; + + // Intra-block rotations: stride * k for k in [1, INTERNAL_GSTEP] + // We need rotations up to the max block size used, which is min(gStep, INTERNAL_GSTEP) + // But to be safe and simple, we can generate up to INTERNAL_GSTEP if gStep >= INTERNAL_GSTEP + uint32_t maxIntra = std::min(gStep, INTERNAL_GSTEP); + for (uint32_t k = 1; k < maxIntra; ++k) { + res.push_back(stride * k); + } + + // Inter-block rotations: stride * INTERNAL_GSTEP * k for k in [1, blockCount - 1] + uint32_t blockCount = (gStep + INTERNAL_GSTEP - 1) / INTERNAL_GSTEP; + if (blockCount > 1) { + int baseRotation = INTERNAL_GSTEP * stride; + for (uint32_t k = 1; k < blockCount; ++k) { + res.push_back(baseRotation * k); + } + } + + return res; +} + +} // namespace fideslib diff --git a/api/HostMath.hpp b/api/HostMath.hpp new file mode 100644 index 00000000..f0caa9ca --- /dev/null +++ b/api/HostMath.hpp @@ -0,0 +1,22 @@ +#ifndef API_HOSTMATH_HPP +#define API_HOSTMATH_HPP + +#include +#include +#include + +namespace fideslib { + +// Pure-host helpers that used to live in CUDA translation units (src/PolyApprox and +// src/CKKS/LinearTransform). They contain no device code, so they are compiled in every +// build and the facade's static wrappers forward to them. + +/// @brief Discrete Chebyshev approximation coefficients of `func` on [a, b]. +std::vector get_chebyshev_coefficients(const std::function& func, double a, double b, uint32_t degree); + +/// @brief BSGS rotation indices required by the convolution transform. +std::vector GetConvolutionTransformRotationIndices(int rowSize, int bStep, int stride, uint32_t gStep); + +} // namespace fideslib + +#endif // API_HOSTMATH_HPP diff --git a/api/Plaintext.cpp b/api/Plaintext.cpp index 196f52cf..95103161 100644 --- a/api/Plaintext.cpp +++ b/api/Plaintext.cpp @@ -12,56 +12,49 @@ PlaintextImpl::PlaintextImpl(const CryptoContext&& context) } } -PlaintextImpl::~PlaintextImpl() { - if (this->loaded && this->gpu != 0 && this->parent_context) { - this->parent_context->EvictDevicePlaintext(this->gpu); - this->gpu = 0; - } -} - // ---- Functions ---- void PlaintextImpl::SetLength(size_t length) { - if (this->cpu.has_value()) { - auto& impl = std::any_cast(this->cpu); + if (this->host.has_value()) { + auto& impl = std::any_cast(this->host); impl->SetLength(length); } } void PlaintextImpl::SetSlots(uint32_t slots) { - if (this->cpu.has_value()) { - auto& impl = std::any_cast(this->cpu); + if (this->host.has_value()) { + auto& impl = std::any_cast(this->host); impl->SetSlots(slots); } } double PlaintextImpl::GetLogPrecision() const { - if (this->cpu.has_value()) { - auto& impl = std::any_cast(this->cpu); + if (this->host.has_value()) { + auto& impl = std::any_cast(this->host); return impl->GetLogPrecision(); } return 0.0; } uint32_t PlaintextImpl::GetLevel() const { - if (this->cpu.has_value()) { - auto& impl = std::any_cast(this->cpu); + if (this->host.has_value()) { + auto& impl = std::any_cast(this->host); return impl->GetLevel(); } return 0; } std::vector> PlaintextImpl::GetCKKSPackedValue() const { - if (this->cpu.has_value()) { - auto& impl = std::any_cast(this->cpu); + if (this->host.has_value()) { + auto& impl = std::any_cast(this->host); return impl->GetCKKSPackedValue(); } return {}; } std::vector PlaintextImpl::GetRealPackedValue() const { - if (this->cpu.has_value()) { - auto& impl = std::any_cast(this->cpu); + if (this->host.has_value()) { + auto& impl = std::any_cast(this->host); return impl->GetRealPackedValue(); } return {}; @@ -70,8 +63,8 @@ std::vector PlaintextImpl::GetRealPackedValue() const { // ---- Friend Operators ---- std::ostream& operator<<(std::ostream& os, const PlaintextImpl& pt) { - if (pt.cpu.has_value()) { - const auto& impl = std::any_cast(pt.cpu); + if (pt.host.has_value()) { + const auto& impl = std::any_cast(pt.host); os << impl; } else { os << "Empty Plaintext"; @@ -80,8 +73,8 @@ std::ostream& operator<<(std::ostream& os, const PlaintextImpl& pt) { } std::ostream& operator<<(std::ostream& os, const Plaintext& pt) { - if (pt && pt->cpu.has_value()) { - const auto& impl = std::any_cast(pt->cpu); + if (pt && pt->host.has_value()) { + const auto& impl = std::any_cast(pt->host); os << impl; } else { os << "Empty Plaintext"; diff --git a/api/Plaintext.hpp b/api/Plaintext.hpp index 54b2d950..85cdc5ae 100644 --- a/api/Plaintext.hpp +++ b/api/Plaintext.hpp @@ -15,7 +15,7 @@ namespace fideslib { class PlaintextImpl { public: PlaintextImpl() = default; - ~PlaintextImpl(); + ~PlaintextImpl() = default; // device slot frees its backend payload via RAII PlaintextImpl(const CryptoContext&& context); @@ -49,11 +49,11 @@ class PlaintextImpl { // ---- Internal State ---- - std::any cpu; - uint32_t gpu = 0; - /// @brief Flag indicating whether the plaintext is loaded to the devices. - bool loaded = false; - /// @brief Parent context for device management. + /// @brief Canonical host value (lbcrypto::Plaintext). + std::any host; + /// @brief Backend-resident payload (the CUDA backend stores a shared_ptr to its device plaintext); empty == not resident. + std::any device; + /// @brief Parent context. CryptoContext parent_context; }; diff --git a/api/Serialize.cpp b/api/Serialize.cpp index 68c047ee..87a31690 100644 --- a/api/Serialize.cpp +++ b/api/Serialize.cpp @@ -8,11 +8,13 @@ #include "PrivateKey.hpp" #include "PublicKey.hpp" #include "Serialize.hpp" +#include "engine/Backend.hpp" +#include "engine/Engine.hpp" namespace fideslib::Serial { bool SerializeToFile(const std::string& filename, const fideslib::CryptoContext& obj, const SerType& sertype) { - auto& context = std::any_cast&>(obj->cpu); + auto& context = std::any_cast&>(obj->host); bool res = false; switch (sertype) { @@ -31,7 +33,7 @@ bool SerializeToFile(const std::string& filename, const fideslib::CryptoContext< return false; } - // Serialize GPU device information. + // Serialize device and context metadata. std::string filename_dev = filename + ".dev"; std::ofstream devFile(filename_dev, std::ios::binary); if (!devFile.is_open()) { @@ -39,8 +41,13 @@ bool SerializeToFile(const std::string& filename, const fideslib::CryptoContext< return false; } - std::string devData = std::to_string(obj->devices.size()) + " { "; - for (const auto& dev : obj->devices) { + // Persist the backend explicitly (never inferred from the device list on read-back). + std::string backendData = "Backend: " + std::to_string(static_cast(obj->engine_->backend())) + "\n"; + devFile.write(backendData.c_str(), backendData.size()); + // Device list (meaningful only for the CUDA backend). + const std::vector devices = obj->engine_->devices(); + std::string devData = std::to_string(devices.size()) + " { "; + for (const auto& dev : devices) { devData += std::to_string(dev) + " "; } devData += "}\n"; @@ -121,12 +128,15 @@ bool DeserializeFromFile(const std::string& filename, fideslib::CryptoContext gpu_context; - gpu_context.cpu = std::make_any>(context); - gpu_context.devices = std::vector(); - gpu_context.multiplicative_depth = context->GetCryptoParameters()->GetElementParams()->GetParams().size() - 1; - auto ptr = std::make_shared>(std::move(gpu_context)); - ptr->self_reference = std::weak_ptr>(ptr); - obj = ptr; + gpu_context.host = std::make_any>(context); + gpu_context.multiplicative_depth = context->GetCryptoParameters()->GetElementParams()->GetParams().size() - 1; + auto ptr = std::make_shared>(std::move(gpu_context)); + ptr->self_reference = std::weak_ptr>(ptr); + // Install a valid (CPU) engine up front so the context never dispatches through a + // null engine_ if an error path below returns early; overwritten with the + // serialized backend once the .dev metadata is parsed. + ptr->engine_ = MakeEngine(Backend::CPU); + obj = ptr; // Deserialize GPU device information if available. std::ifstream devFile(filename + ".dev", std::ios::binary); @@ -134,22 +144,34 @@ bool DeserializeFromFile(const std::string& filename, fideslib::CryptoContext> label >> b; // "Backend:" + backend = static_cast(b); + } + // Second line: device IDs. + std::vector devices; if (std::getline(devFile, line)) { std::istringstream iss(line); size_t numDevices; iss >> numDevices; char brace; iss >> brace; // Read '{' - ptr->devices.clear(); for (size_t i = 0; i < numDevices; ++i) { int devId; iss >> devId; - ptr->devices.push_back(devId); + devices.push_back(devId); } } - // Second line: AutoLoadCiphertexts + // Rebuild the engine for the serialized backend and hand it the device list. + ptr->engine_ = MakeEngine(backend); + ptr->engine_->setDevices(devices); + // Third line: AutoLoadCiphertexts if (std::getline(devFile, line)) { std::istringstream iss(line); std::string label; @@ -158,7 +180,7 @@ bool DeserializeFromFile(const std::string& filename, fideslib::CryptoContext> flag; ptr->auto_load_ciphertexts = (flag != 0); } - // Third line: AutoLoadPlaintexts + // Fourth line: AutoLoadPlaintexts if (std::getline(devFile, line)) { std::istringstream iss(line); std::string label; @@ -167,7 +189,7 @@ bool DeserializeFromFile(const std::string& filename, fideslib::CryptoContext> flag; ptr->auto_load_plaintexts = (flag != 0); } - // Fourth line: RotationIndexes + // Fifth line: RotationIndexes if (std::getline(devFile, line)) { std::istringstream iss(line); std::string label; @@ -180,7 +202,7 @@ bool DeserializeFromFile(const std::string& filename, fideslib::CryptoContextrotation_indexes.push_back(index); } } - // Fifth line: KeyDist + // Sixth line: KeyDist if (std::getline(devFile, line)) { std::istringstream iss(line); std::string label; @@ -189,7 +211,7 @@ bool DeserializeFromFile(const std::string& filename, fideslib::CryptoContext> dist; ptr->keyDist = (SecretKeyDist)dist; } - // Sixth line: Boostrap slots + // Seventh line: Bootstrap slots if (std::getline(devFile, line)) { std::istringstream iss(line); std::string label; diff --git a/api/engine/Backend.cpp b/api/engine/Backend.cpp new file mode 100644 index 00000000..70786621 --- /dev/null +++ b/api/engine/Backend.cpp @@ -0,0 +1,27 @@ +#include "engine/Backend.hpp" + +#include "engine/Engine.hpp" +#include "engine/cpu/OpenFheEngine.hpp" +#include "engine/cuda/CudaEngine.hpp" + +#include + +namespace fideslib { + +bool IsBackendAvailable(Backend backend) { + switch (backend) { + case Backend::CPU: return true; + case Backend::CUDA: return true; + } + return false; +} + +std::unique_ptr MakeEngine(Backend backend) { + switch (backend) { + case Backend::CPU: return std::make_unique(); + case Backend::CUDA: return std::make_unique(); + } + throw std::runtime_error("unknown backend"); +} + +} // namespace fideslib diff --git a/api/engine/Backend.hpp b/api/engine/Backend.hpp new file mode 100644 index 00000000..c25dba5a --- /dev/null +++ b/api/engine/Backend.hpp @@ -0,0 +1,25 @@ +#ifndef API_BACKEND_HPP +#define API_BACKEND_HPP + +#include + +namespace fideslib { + +/// @brief Execution backend, chosen at runtime. The enum is always declared; +/// availability depends on what was compiled in (see IsBackendAvailable). +enum class Backend { + CPU, ///< OpenFHE on the host. Always available. + CUDA, ///< FIDESlib CUDA implementation. Available iff built with CUDA. +}; + +class Engine; + +/// @brief Whether a backend was compiled into this build. +bool IsBackendAvailable(Backend backend); + +/// @brief Construct the engine for a backend; throws if it was not compiled in. +std::unique_ptr MakeEngine(Backend backend); + +} // namespace fideslib + +#endif diff --git a/api/engine/Engine.cpp b/api/engine/Engine.cpp new file mode 100644 index 00000000..68689bfc --- /dev/null +++ b/api/engine/Engine.cpp @@ -0,0 +1,275 @@ +#include "engine/Engine.hpp" + +#include + +#include + +namespace fideslib { + +// Default implementations: every operation throws until a backend overrides it, so a backend under +// construction compiles (and reports a precise error at runtime) instead of having to stub the whole +// interface up front. teardown() is the one exception — see below. + +void Engine::notImplemented(const char* op) const { + OPENFHE_THROW(std::string(op) + " is not implemented by the " + name() + " backend"); +} + +// ---- Negation ---- +Ciphertext Engine::evalNegate(CryptoContextImpl&, const Ciphertext&) { + notImplemented("evalNegate"); +} + +void Engine::evalNegateInPlace(CryptoContextImpl&, Ciphertext&) { + notImplemented("evalNegateInPlace"); +} + +// ---- Addition ---- +Ciphertext Engine::evalAdd(CryptoContextImpl&, const Ciphertext&, const Ciphertext&) { + notImplemented("evalAdd"); +} + +void Engine::evalAddInPlace(CryptoContextImpl&, Ciphertext&, const Ciphertext&) { + notImplemented("evalAddInPlace"); +} + +Ciphertext Engine::evalAdd(CryptoContextImpl&, const Ciphertext&, Plaintext&) { + notImplemented("evalAdd"); +} + +Ciphertext Engine::evalAdd(CryptoContextImpl&, const Ciphertext&, double) { + notImplemented("evalAdd"); +} + +void Engine::evalAddInPlace(CryptoContextImpl&, Ciphertext&, Plaintext&) { + notImplemented("evalAddInPlace"); +} + +void Engine::evalAddInPlace(CryptoContextImpl&, Ciphertext&, double) { + notImplemented("evalAddInPlace"); +} + +Ciphertext Engine::evalAddMany(CryptoContextImpl&, const std::vector>&) { + notImplemented("evalAddMany"); +} + +void Engine::evalAddManyInPlace(CryptoContextImpl&, std::vector>&) { + notImplemented("evalAddManyInPlace"); +} + +// ---- Subtraction ---- +Ciphertext Engine::evalSub(CryptoContextImpl&, const Ciphertext&, const Ciphertext&) { + notImplemented("evalSub"); +} + +Ciphertext Engine::evalSub(CryptoContextImpl&, const Ciphertext&, Plaintext&) { + notImplemented("evalSub"); +} + +Ciphertext Engine::evalSub(CryptoContextImpl&, Plaintext&, const Ciphertext&) { + notImplemented("evalSub"); +} + +Ciphertext Engine::evalSub(CryptoContextImpl&, const Ciphertext&, double) { + notImplemented("evalSub"); +} + +Ciphertext Engine::evalSub(CryptoContextImpl&, double, const Ciphertext&) { + notImplemented("evalSub"); +} + +void Engine::evalSubInPlace(CryptoContextImpl&, Ciphertext&, const Ciphertext&) { + notImplemented("evalSubInPlace"); +} + +void Engine::evalSubInPlace(CryptoContextImpl&, Ciphertext&, double) { + notImplemented("evalSubInPlace"); +} + +void Engine::evalSubInPlace(CryptoContextImpl&, double, Ciphertext&) { + notImplemented("evalSubInPlace"); +} + +// ---- Multiplication ---- +Ciphertext Engine::evalMult(CryptoContextImpl&, const Ciphertext&, const Ciphertext&) { + notImplemented("evalMult"); +} + +Ciphertext Engine::evalMult(CryptoContextImpl&, const Ciphertext&, Plaintext&) { + notImplemented("evalMult"); +} + +Ciphertext Engine::evalMult(CryptoContextImpl&, const Ciphertext&, double) { + notImplemented("evalMult"); +} + +void Engine::evalMultInPlace(CryptoContextImpl&, Ciphertext&, Plaintext&) { + notImplemented("evalMultInPlace"); +} + +void Engine::evalMultInPlace(CryptoContextImpl&, Ciphertext&, double) { + notImplemented("evalMultInPlace"); +} + +void Engine::evalMultInPlace(CryptoContextImpl&, Ciphertext&, Ciphertext&) { + notImplemented("evalMultInPlace"); +} + +// ---- Square ---- +Ciphertext Engine::evalSquare(CryptoContextImpl&, const Ciphertext&) { + notImplemented("evalSquare"); +} + +void Engine::evalSquareInPlace(CryptoContextImpl&, Ciphertext&) { + notImplemented("evalSquareInPlace"); +} + +// ---- Rotation ---- +Ciphertext Engine::evalRotate(CryptoContextImpl&, const Ciphertext&, int32_t) { + notImplemented("evalRotate"); +} + +void Engine::evalRotateInPlace(CryptoContextImpl&, Ciphertext&, int32_t) { + notImplemented("evalRotateInPlace"); +} + +std::shared_ptr Engine::evalFastRotationPrecompute(CryptoContextImpl&, const Ciphertext&) { + notImplemented("evalFastRotationPrecompute"); +} + +Ciphertext Engine::evalFastRotation(CryptoContextImpl&, const Ciphertext&, const int32_t, const uint32_t, const std::shared_ptr&) { + notImplemented("evalFastRotation"); +} + +Ciphertext Engine::evalFastRotationExt(CryptoContextImpl&, const Ciphertext&, const int32_t, const std::shared_ptr&, bool) { + notImplemented("evalFastRotationExt"); +} + +std::vector> +Engine::evalFastRotation(CryptoContextImpl&, const Ciphertext&, const std::vector&, const uint32_t, const std::shared_ptr&) { + notImplemented("evalFastRotation"); +} + +std::vector> +Engine::evalFastRotationExt(CryptoContextImpl&, const Ciphertext&, const std::vector&, const std::shared_ptr&, bool) { + notImplemented("evalFastRotationExt"); +} + +// ---- Chebyshev series ---- +Ciphertext Engine::evalChebyshevSeries(CryptoContextImpl&, const Ciphertext&, std::vector&, double, double) { + notImplemented("evalChebyshevSeries"); +} + +void Engine::evalChebyshevSeriesInPlace(CryptoContextImpl&, Ciphertext&, std::vector&, double, double) { + notImplemented("evalChebyshevSeriesInPlace"); +} + +// ---- Rescale ---- +Ciphertext Engine::rescale(CryptoContextImpl&, const Ciphertext&) { + notImplemented("rescale"); +} + +void Engine::rescaleInPlace(CryptoContextImpl&, Ciphertext&) { + notImplemented("rescaleInPlace"); +} + +// ---- Accumulate (sum reduction) ---- +Ciphertext Engine::accumulateSum(CryptoContextImpl&, const Ciphertext&, int, int) { + notImplemented("accumulateSum"); +} + +void Engine::accumulateSumInPlace(CryptoContextImpl&, Ciphertext&, int, int) { + notImplemented("accumulateSumInPlace"); +} + +void Engine::accumulateSumInPlace(CryptoContextImpl&, Ciphertext&, int, int, int) { + notImplemented("accumulateSumInPlace"); +} + +// ---- Bootstrapping ---- +BootstrapSetupPolicy Engine::bootstrapSetupPolicy(bool, bool, int32_t) const { + notImplemented("bootstrapSetupPolicy"); +} + +void Engine::evalBootstrapKeyGen(CryptoContextImpl&, const PrivateKey&, uint32_t) { + notImplemented("evalBootstrapKeyGen"); +} + +Ciphertext Engine::evalBootstrap(CryptoContextImpl&, const Ciphertext&, uint32_t, uint32_t, bool) { + notImplemented("evalBootstrap"); +} + +void Engine::evalBootstrapInPlace(CryptoContextImpl&, Ciphertext&, uint32_t, uint32_t, bool) { + notImplemented("evalBootstrapInPlace"); +} + +// ---- Convolution transform (BSGS linear transform) ---- +void Engine::convolutionTransformInPlace(CryptoContextImpl&, Ciphertext&, int, int, const std::vector&, const std::vector<int>&, int, int) { + notImplemented("convolutionTransformInPlace"); +} + +void Engine::specialConvolutionTransformInPlace(CryptoContextImpl<DCRTPoly>&, Ciphertext<DCRTPoly>&, int, int, const std::vector<Plaintext>&, Plaintext&, const std::vector<int>&, int, int, int) { + notImplemented("specialConvolutionTransformInPlace"); +} + +// ---- Host readback ---- +void Engine::recoverHostCiphertext(CryptoContextImpl<DCRTPoly>&, Ciphertext<DCRTPoly>&) { + notImplemented("recoverHostCiphertext"); +} + +// ---- Device residency ---- +void Engine::loadContext(CryptoContextImpl<DCRTPoly>&, const PublicKey<DCRTPoly>&) { + notImplemented("loadContext"); +} + +void Engine::loadPlaintext(CryptoContextImpl<DCRTPoly>&, Plaintext&) { + notImplemented("loadPlaintext"); +} + +void Engine::loadCiphertext(CryptoContextImpl<DCRTPoly>&, Ciphertext<DCRTPoly>&) { + notImplemented("loadCiphertext"); +} + +// ---- Ciphertext backend hooks ---- +std::any Engine::cloneCiphertextBackend(CryptoContextImpl<DCRTPoly>&, const CiphertextImpl<DCRTPoly>&) { + notImplemented("cloneCiphertextBackend"); +} + +size_t Engine::ciphertextLevel(CryptoContextImpl<DCRTPoly>&, const CiphertextImpl<DCRTPoly>&) { + notImplemented("ciphertextLevel"); +} + +size_t Engine::ciphertextNoiseScaleDeg(CryptoContextImpl<DCRTPoly>&, const CiphertextImpl<DCRTPoly>&) { + notImplemented("ciphertextNoiseScaleDeg"); +} + +void Engine::setCiphertextSlots(CryptoContextImpl<DCRTPoly>&, CiphertextImpl<DCRTPoly>&, size_t) { + notImplemented("setCiphertextSlots"); +} + +void Engine::setCiphertextLevel(CryptoContextImpl<DCRTPoly>&, CiphertextImpl<DCRTPoly>&, size_t) { + notImplemented("setCiphertextLevel"); +} + +// ---- Context backend state ---- +bool Engine::isContextLoaded() const { + notImplemented("isContextLoaded"); +} + +void Engine::synchronize() const { + notImplemented("synchronize"); +} + +// The context destructor calls teardown() unconditionally, so the default must succeed (a backend with +// no device state simply has nothing to tear down) — a throw here would terminate the program. +void Engine::teardown() { +} + +void Engine::setDevices(const std::vector<int>&) { + notImplemented("setDevices"); +} + +std::vector<int> Engine::devices() const { + notImplemented("devices"); +} + +} // namespace fideslib diff --git a/api/engine/Engine.hpp b/api/engine/Engine.hpp new file mode 100644 index 00000000..1de10f91 --- /dev/null +++ b/api/engine/Engine.hpp @@ -0,0 +1,170 @@ +#ifndef API_ENGINE_HPP +#define API_ENGINE_HPP + +#include "Ciphertext.hpp" +#include "Plaintext.hpp" +#include "PublicKey.hpp" +#include "engine/Backend.hpp" + +#include <any> +#include <cstddef> +#include <cstdint> +#include <vector> + +namespace fideslib { + +template <typename T> class CryptoContextImpl; + +/// @brief Per-backend arguments for the host-side OpenFHE EvalBootstrapSetup call, which the facade +/// performs. The engine only supplies this policy; it does no bootstrap-setup work itself. +struct BootstrapSetupPolicy { + bool precompute; + bool btSlotsEncoding; + int32_t modEvalLevels; +}; + +/// @brief One implementation per backend. Stateless: each method receives the +/// owning context. A backend's method bodies live entirely in its own +/// translation unit (engine/cpu for the CPU/OpenFHE backend, engine/cuda for the +/// CUDA backend), so CPU and CUDA code are separated by file rather than +/// interleaved per function. +class Engine { + protected: + /// @brief Backing for the default implementations: throws "<op> is not implemented by the + /// <name()> backend". + [[noreturn]] void notImplemented(const char* op) const; + + public: + // ---- Lifecycle ---- + virtual ~Engine() = default; + virtual const char* name() const = 0; + /// @brief Which backend this engine is (for explicit serialization; never inferred from devices). + virtual Backend backend() const = 0; + + // ---- Negation ---- + virtual Ciphertext<DCRTPoly> evalNegate(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct); + virtual void evalNegateInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct); + + // ---- Addition ---- + virtual Ciphertext<DCRTPoly> evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2); + virtual void evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2); + virtual Ciphertext<DCRTPoly> evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, Plaintext& pt); + virtual Ciphertext<DCRTPoly> evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, double scalar); + virtual void evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Plaintext& pt); + virtual void evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar); + virtual Ciphertext<DCRTPoly> evalAddMany(CryptoContextImpl<DCRTPoly>& ctx, const std::vector<Ciphertext<DCRTPoly>>& ciphertexts); + virtual void evalAddManyInPlace(CryptoContextImpl<DCRTPoly>& ctx, std::vector<Ciphertext<DCRTPoly>>& ciphertexts); + + // ---- Subtraction ---- + virtual Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2); + virtual Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, Plaintext& pt); + virtual Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, Plaintext& pt, const Ciphertext<DCRTPoly>& ct); + virtual Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, double scalar); + virtual Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, double scalar, const Ciphertext<DCRTPoly>& ct); + virtual void evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2); + virtual void evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar); + virtual void evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, double scalar, Ciphertext<DCRTPoly>& ct1); + + // ---- Multiplication ---- + virtual Ciphertext<DCRTPoly> evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2); + virtual Ciphertext<DCRTPoly> evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, Plaintext& pt); + virtual Ciphertext<DCRTPoly> evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, double scalar); + virtual void evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Plaintext& pt); + virtual void evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar); + virtual void evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Ciphertext<DCRTPoly>& ct2); + + // ---- Square ---- + virtual Ciphertext<DCRTPoly> evalSquare(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct); + virtual void evalSquareInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct); + + // ---- Rotation ---- + virtual Ciphertext<DCRTPoly> evalRotate(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext, int32_t index); + virtual void evalRotateInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext, int32_t index); + virtual std::shared_ptr<void> evalFastRotationPrecompute(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct); + virtual Ciphertext<DCRTPoly> + evalFastRotation(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const int32_t index, const uint32_t m, const std::shared_ptr<void>& precomp); + virtual Ciphertext<DCRTPoly> + evalFastRotationExt(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const int32_t index, const std::shared_ptr<void>& digits, bool addFirst); + virtual std::vector<Ciphertext<DCRTPoly>> + evalFastRotation(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const std::vector<int32_t>& indices, const uint32_t m, const std::shared_ptr<void>& precomp); + virtual std::vector<Ciphertext<DCRTPoly>> + evalFastRotationExt(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const std::vector<int32_t>& indices, const std::shared_ptr<void>& digits, bool addFirst); + + // ---- Chebyshev series ---- + virtual Ciphertext<DCRTPoly> evalChebyshevSeries(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, std::vector<double>& coeffs, double a, double b); + virtual void evalChebyshevSeriesInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, std::vector<double>& coeffs, double a, double b); + + // ---- Rescale ---- + virtual Ciphertext<DCRTPoly> rescale(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext); + virtual void rescaleInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext); + + // ---- Accumulate (sum reduction) ---- + virtual Ciphertext<DCRTPoly> accumulateSum(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, int slots, int stride); + virtual void accumulateSumInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, int slots, int stride); + virtual void accumulateSumInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, int slots, int stride, int start); + + // ---- Bootstrapping ---- + /// @brief Per-backend arguments for the facade's host EvalBootstrapSetup call (the setup itself is a + /// host computation and lives on the facade, not here). + virtual BootstrapSetupPolicy bootstrapSetupPolicy(bool precompute, bool btsfirstboot, int32_t modEvalLevels) const; + virtual void evalBootstrapKeyGen(CryptoContextImpl<DCRTPoly>& ctx, const PrivateKey<DCRTPoly>& secretKey, uint32_t slots); + virtual Ciphertext<DCRTPoly> + evalBootstrap(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled); + virtual void evalBootstrapInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled); + + // ---- Convolution transform (BSGS linear transform) ---- + virtual void convolutionTransformInPlace(CryptoContextImpl<DCRTPoly>& ctx, + Ciphertext<DCRTPoly>& ct, + int gStep, + int bStep, + const std::vector<Plaintext>& pts, + const std::vector<int>& indexes, + int stride, + int rowSize); + virtual void specialConvolutionTransformInPlace(CryptoContextImpl<DCRTPoly>& ctx, + Ciphertext<DCRTPoly>& ct, + int gStep, + int bStep, + const std::vector<Plaintext>& pts, + Plaintext& mask, + const std::vector<int>& indexes, + int stride, + int maskRotationStride, + int rowSize); + + // ---- Host readback ---- + // Refresh ct->host with the backend's current value, syncing it back from the device if resident, so a + // caller can recover the underlying lbcrypto::Ciphertext without decrypting. This does NOT evict — + // ct->device stays resident for further ops. CPU: no-op (already on host). CUDA: store() + + // GetOpenFHECipherText, growing the host container to the device limb count when needed (e.g. after a + // level-raising ModRaise). + virtual void recoverHostCiphertext(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct); + + // ---- Device residency (CUDA-only; the CPU backend implements these as no-ops) ---- + virtual void loadContext(CryptoContextImpl<DCRTPoly>& ctx, const PublicKey<DCRTPoly>& publicKey); + virtual void loadPlaintext(CryptoContextImpl<DCRTPoly>& ctx, Plaintext& pt); + virtual void loadCiphertext(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct); + + // ---- Ciphertext backend hooks ---- + // The neutral value type holds only its canonical host value plus an opaque `device` slot; all + // backend manipulation of that slot happens here. The CPU backend reads the host value; the CUDA + // backend reads/writes ct.device. The value types reach these through thin facade methods. + // Clone the backend-resident payload of `src` into a fresh `device` slot (empty any == not resident). + virtual std::any cloneCiphertextBackend(CryptoContextImpl<DCRTPoly>& ctx, const CiphertextImpl<DCRTPoly>& src); + virtual size_t ciphertextLevel(CryptoContextImpl<DCRTPoly>& ctx, const CiphertextImpl<DCRTPoly>& ct); + virtual size_t ciphertextNoiseScaleDeg(CryptoContextImpl<DCRTPoly>& ctx, const CiphertextImpl<DCRTPoly>& ct); + virtual void setCiphertextSlots(CryptoContextImpl<DCRTPoly>& ctx, CiphertextImpl<DCRTPoly>& ct, size_t slots); + virtual void setCiphertextLevel(CryptoContextImpl<DCRTPoly>& ctx, CiphertextImpl<DCRTPoly>& ct, size_t level); + + // ---- Context backend state (no ctx argument; each context owns its engine) ---- + virtual bool isContextLoaded() const; + virtual void synchronize() const; + /// @brief Default is a no-op, not a throw: the context destructor calls this unconditionally. + virtual void teardown(); + virtual void setDevices(const std::vector<int>& devices); + virtual std::vector<int> devices() const; +}; + +} // namespace fideslib + +#endif diff --git a/api/engine/EngineCommon.cpp b/api/engine/EngineCommon.cpp new file mode 100644 index 00000000..4fda46f5 --- /dev/null +++ b/api/engine/EngineCommon.cpp @@ -0,0 +1,27 @@ +#include "engine/EngineCommon.hpp" + +#include <openfhe.h> + +#include <vector> + +namespace fideslib { + +int32_t bootstrapModEvalLevels(SecretKeyDist keyDist) { + std::vector<double> coeffchebyshev; + int doubleAngleIts = 3; + if (keyDist == SPARSE_ENCAPSULATED) { + coeffchebyshev = lbcrypto::FHECKKSRNS::g_coefficientsSparseEncapsulated; + doubleAngleIts = lbcrypto::FHECKKSRNS::R_SPARSE; + } else if (keyDist == SPARSE_TERNARY) { + coeffchebyshev = lbcrypto::FHECKKSRNS::g_coefficientsSparse; + doubleAngleIts = lbcrypto::FHECKKSRNS::R_SPARSE; + } else if (keyDist == UNIFORM_TERNARY) { + coeffchebyshev = lbcrypto::FHECKKSRNS::g_coefficientsUniform; + doubleAngleIts = lbcrypto::FHECKKSRNS::R_UNIFORM; + } else { + OPENFHE_THROW("Unsupported key distribution"); + } + return static_cast<int>(lbcrypto::GetMultiplicativeDepthByCoeffVector(coeffchebyshev, false)) + doubleAngleIts; +} + +} // namespace fideslib diff --git a/api/engine/EngineCommon.hpp b/api/engine/EngineCommon.hpp new file mode 100644 index 00000000..b66987b0 --- /dev/null +++ b/api/engine/EngineCommon.hpp @@ -0,0 +1,17 @@ +#ifndef API_ENGINE_COMMON_HPP +#define API_ENGINE_COMMON_HPP + +#include <cstdint> + +#include "Definitions.hpp" + +namespace fideslib { + +// Number of mod-evaluation levels the bootstrap mod-reduction Chebyshev approximation consumes for +// the given secret-key distribution. Both engines pass this to OpenFHE's EvalBootstrapSetup so the +// CPU and CUDA paths configure mod-evaluation identically. Defined in EngineCommon.cpp. +int32_t bootstrapModEvalLevels(SecretKeyDist keyDist); + +} // namespace fideslib + +#endif // API_ENGINE_COMMON_HPP diff --git a/api/engine/cpu/OpenFheEngine.cpp b/api/engine/cpu/OpenFheEngine.cpp new file mode 100644 index 00000000..4aa8cd39 --- /dev/null +++ b/api/engine/cpu/OpenFheEngine.cpp @@ -0,0 +1,517 @@ +#include "engine/cpu/OpenFheEngine.hpp" + +#include "CryptoContext.hpp" +#include "engine/EngineCommon.hpp" + +#include <openfhe.h> + +#include <algorithm> +#include <any> +#include <cassert> + +namespace fideslib { + +namespace { +// Unwrap the host-side OpenFHE objects from the value types' / context's `host` std::any (parity with +// engine/cuda's deviceCt/devicePt). On the CPU backend `host` always carries the value, so these are the +// single access point; callers that mutate in place still EnsureLazyHostCopy() first. +lbcrypto::CryptoContext<lbcrypto::DCRTPoly>& hostContext(CryptoContextImpl<DCRTPoly>& ctx) { + return std::any_cast<lbcrypto::CryptoContext<lbcrypto::DCRTPoly>&>(ctx.host); +} + +lbcrypto::Ciphertext<lbcrypto::DCRTPoly>& hostCt(const Ciphertext<DCRTPoly>& ct) { + return std::any_cast<lbcrypto::Ciphertext<lbcrypto::DCRTPoly>&>(ct->host); +} + +lbcrypto::Plaintext& hostPt(const Plaintext& pt) { + return std::any_cast<lbcrypto::Plaintext&>(pt->host); +} + +// Store a freshly-computed host OpenFHE ciphertext into an existing value type's `host` slot +// (parity with engine/cuda writing back a device result). The single place that packaging lives. +void setHostCt(const Ciphertext<DCRTPoly>& ct, lbcrypto::Ciphertext<lbcrypto::DCRTPoly> res) { + ct->host = std::make_any<lbcrypto::Ciphertext<lbcrypto::DCRTPoly>>(std::move(res)); +} + +// Wrap a host result in a new value-type Ciphertext parented to `ctx`, then set its `host` slot. +Ciphertext<DCRTPoly> wrapHostCt(CryptoContextImpl<DCRTPoly>& ctx, lbcrypto::Ciphertext<lbcrypto::DCRTPoly> res) { + Ciphertext<DCRTPoly> ct = std::make_shared<CiphertextImpl<DCRTPoly>>(ctx.self_reference.lock()); + setHostCt(ct, std::move(res)); + return ct; +} + +// As above, but the new value type is copy-constructed from `proto` (carrying its metadata) rather +// than freshly parented — used where a result inherits an existing ciphertext's state. +Ciphertext<DCRTPoly> wrapHostCt(const Ciphertext<DCRTPoly>& proto, lbcrypto::Ciphertext<lbcrypto::DCRTPoly> res) { + Ciphertext<DCRTPoly> ct = std::make_shared<CiphertextImpl<DCRTPoly>>(*proto); + setHostCt(ct, std::move(res)); + return ct; +} +} // namespace + +Ciphertext<DCRTPoly> OpenFheEngine::evalNegate(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + return wrapHostCt(ctx, context->EvalNegate(ctImpl)); +} + +void OpenFheEngine::evalNegateInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) { + auto& context = hostContext(ctx); + ct->EnsureLazyHostCopy(); + auto& ctImpl = hostCt(ct); + context->EvalNegateInPlace(ctImpl); +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) { + auto& context = hostContext(ctx); + auto& ct1Impl = hostCt(ct1); + auto& ct2Impl = hostCt(ct2); + return wrapHostCt(ctx, context->EvalAdd(ct1Impl, ct2Impl)); +} + +void OpenFheEngine::evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) { + auto& context = hostContext(ctx); + ct1->EnsureLazyHostCopy(); + auto& ct1Impl = hostCt(ct1); + auto& ct2Impl = hostCt(ct2); + context->EvalAddInPlace(ct1Impl, ct2Impl); +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, Plaintext& pt) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + auto& ptImpl = hostPt(pt); + return wrapHostCt(ctx, context->EvalAdd(ctImpl, ptImpl)); +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, double scalar) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + return wrapHostCt(ctx, context->EvalAdd(ctImpl, scalar)); +} + +void OpenFheEngine::evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Plaintext& pt) { + auto& context = hostContext(ctx); + ct1->EnsureLazyHostCopy(); + auto& ct1Impl = hostCt(ct1); + auto& ptImpl = hostPt(pt); + context->EvalAddInPlace(ct1Impl, ptImpl); + return; +} + +void OpenFheEngine::evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) { + auto& context = hostContext(ctx); + ct1->EnsureLazyHostCopy(); + auto& ct1Impl = hostCt(ct1); + context->EvalAddInPlace(ct1Impl, scalar); + return; +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalAddMany(CryptoContextImpl<DCRTPoly>& ctx, const std::vector<Ciphertext<DCRTPoly>>& ciphertexts) { + if (ciphertexts.empty()) { + OPENFHE_THROW("EvalAddMany: input ciphertext vector is empty"); + } + + auto& context = hostContext(ctx); + std::vector<lbcrypto::Ciphertext<lbcrypto::DCRTPoly>> ctImpls; + ctImpls.reserve(ciphertexts.size()); + for (const auto& ct : ciphertexts) { + ctImpls.push_back(hostCt(ct)); + } + return wrapHostCt(ctx, context->EvalAddMany(ctImpls)); +} + +void OpenFheEngine::evalAddManyInPlace(CryptoContextImpl<DCRTPoly>& ctx, std::vector<Ciphertext<DCRTPoly>>& ciphertexts) { + if (ciphertexts.empty()) { + OPENFHE_THROW("EvalAddManyInPlace: input ciphertext vector is empty"); + } + + auto& context = hostContext(ctx); + std::vector<lbcrypto::Ciphertext<lbcrypto::DCRTPoly>> ctImpls; + ctImpls.reserve(ciphertexts.size()); + for (const auto& ct : ciphertexts) { + ctImpls.push_back(hostCt(ct)); + } + context->EvalAddManyInPlace(ctImpls); + setHostCt(ciphertexts[0], ctImpls[0]); + return; +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) { + auto& context = hostContext(ctx); + auto& ct1Impl = hostCt(ct1); + auto& ct2Impl = hostCt(ct2); + return wrapHostCt(ctx, context->EvalSub(ct1Impl, ct2Impl)); +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, Plaintext& pt) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + auto& ptImpl = hostPt(pt); + return wrapHostCt(ctx, context->EvalSub(ctImpl, ptImpl)); +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalSub(CryptoContextImpl<DCRTPoly>& ctx, Plaintext& pt, const Ciphertext<DCRTPoly>& ct) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + auto& ptImpl = hostPt(pt); + return wrapHostCt(ctx, context->EvalSub(ptImpl, ctImpl)); +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, double scalar) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + return wrapHostCt(ctx, context->EvalSub(ctImpl, scalar)); +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalSub(CryptoContextImpl<DCRTPoly>& ctx, double scalar, const Ciphertext<DCRTPoly>& ct) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + return wrapHostCt(ctx, context->EvalSub(scalar, ctImpl)); +} + +void OpenFheEngine::evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) { + auto& context = hostContext(ctx); + ct1->EnsureLazyHostCopy(); + auto& ct1Impl = hostCt(ct1); + auto& ct2Impl = hostCt(ct2); + context->EvalSubInPlace(ct1Impl, ct2Impl); + return; +} + +void OpenFheEngine::evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) { + auto& context = hostContext(ctx); + ct1->EnsureLazyHostCopy(); + auto& ct1Impl = hostCt(ct1); + context->EvalSubInPlace(ct1Impl, scalar); + return; +} + +void OpenFheEngine::evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, double scalar, Ciphertext<DCRTPoly>& ct1) { + auto& context = hostContext(ctx); + ct1->EnsureLazyHostCopy(); + auto& ct1Impl = hostCt(ct1); + context->EvalSubInPlace(scalar, ct1Impl); + return; +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) { + auto& context = hostContext(ctx); + auto& ct1Impl = hostCt(ct1); + auto& ct2Impl = hostCt(ct2); + return wrapHostCt(ctx, context->EvalMult(ct1Impl, ct2Impl)); +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, Plaintext& pt) { + auto& context = hostContext(ctx); + auto& ct1Impl = hostCt(ct1); + auto& ptImpl = hostPt(pt); + return wrapHostCt(ctx, context->EvalMult(ct1Impl, ptImpl)); +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, double scalar) { + auto& context = hostContext(ctx); + auto& ct1Impl = hostCt(ct1); + return wrapHostCt(ctx, context->EvalMult(ct1Impl, scalar)); +} + +void OpenFheEngine::evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Plaintext& pt) { + auto& context = hostContext(ctx); + ct1->EnsureLazyHostCopy(); + auto& ct1Impl = hostCt(ct1); + auto& ptImpl = hostPt(pt); + setHostCt(ct1, context->EvalMult(ct1Impl, ptImpl)); + return; +} + +void OpenFheEngine::evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) { + auto& context = hostContext(ctx); + ct1->EnsureLazyHostCopy(); + auto& ct1Impl = hostCt(ct1); + context->EvalMultInPlace(ct1Impl, scalar); + return; +} + +void OpenFheEngine::evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Ciphertext<DCRTPoly>& ct2) { + auto& context = hostContext(ctx); + ct1->EnsureLazyHostCopy(); + ct2->EnsureLazyHostCopy(); + auto& ct1Impl = hostCt(ct1); + auto& ct2Impl = hostCt(ct2); + context->EvalMultMutableInPlace(ct1Impl, ct2Impl); + return; +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalSquare(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + return wrapHostCt(ctx, context->EvalSquare(ctImpl)); +} + +void OpenFheEngine::evalSquareInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) { + auto& context = hostContext(ctx); + ct->EnsureLazyHostCopy(); + auto& ctImpl = hostCt(ct); + context->EvalSquareInPlace(ctImpl); + return; +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalRotate(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext, int32_t index) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ciphertext); + return wrapHostCt(ctx, context->EvalRotate(ctImpl, index)); +} + +void OpenFheEngine::evalRotateInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext, int32_t index) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ciphertext); + setHostCt(ciphertext, context->EvalRotate(ctImpl, index)); + return; +} + +Ciphertext<DCRTPoly> +OpenFheEngine::evalFastRotation(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const int32_t index, const uint32_t m, const std::shared_ptr<void>& precomp) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + auto casted = std::static_pointer_cast<std::vector<lbcrypto::DCRTPolyImpl<bigintdyn::mubintvec<bigintdyn::ubint<unsigned long>>>>>(precomp); + return wrapHostCt(ctx, context->EvalFastRotation(ctImpl, index, m, casted)); +} + +Ciphertext<DCRTPoly> +OpenFheEngine::evalFastRotationExt(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const int32_t index, const std::shared_ptr<void>& digits, bool addFirst) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + auto casted = std::static_pointer_cast<std::vector<lbcrypto::DCRTPolyImpl<bigintdyn::mubintvec<bigintdyn::ubint<unsigned long>>>>>(digits); + return wrapHostCt(ctx, context->EvalFastRotationExt(ctImpl, index, casted, addFirst)); +} + +std::vector<Ciphertext<DCRTPoly>> OpenFheEngine::evalFastRotation(CryptoContextImpl<DCRTPoly>& ctx, + const Ciphertext<DCRTPoly>& ct, + const std::vector<int32_t>& indices, + const uint32_t m, + const std::shared_ptr<void>& precomp) { + std::vector<Ciphertext<DCRTPoly>> results; + + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + auto casted = std::static_pointer_cast<std::vector<lbcrypto::DCRTPolyImpl<bigintdyn::mubintvec<bigintdyn::ubint<unsigned long>>>>>(precomp); + + for (const auto& index : indices) { + results.push_back(wrapHostCt(ct, context->EvalFastRotation(ctImpl, index, m, casted))); + } + return results; +} + +std::vector<Ciphertext<DCRTPoly>> OpenFheEngine::evalFastRotationExt(CryptoContextImpl<DCRTPoly>& ctx, + const Ciphertext<DCRTPoly>& ct, + const std::vector<int32_t>& indices, + const std::shared_ptr<void>& digits, + bool addFirst) { + std::vector<Ciphertext<DCRTPoly>> results; + + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + auto casted = std::static_pointer_cast<std::vector<lbcrypto::DCRTPolyImpl<bigintdyn::mubintvec<bigintdyn::ubint<unsigned long>>>>>(digits); + + for (const auto& index : indices) { + results.push_back(wrapHostCt(ct, context->EvalFastRotationExt(ctImpl, index, casted, addFirst))); + } + return results; +} + +Ciphertext<DCRTPoly> OpenFheEngine::evalChebyshevSeries(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, std::vector<double>& coeffs, double a, double b) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + return wrapHostCt(ctx, context->EvalChebyshevSeries(ctImpl, coeffs, a, b)); +} + +void OpenFheEngine::evalChebyshevSeriesInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, std::vector<double>& coeffs, double a, double b) { + auto& context = hostContext(ctx); + ct->EnsureLazyHostCopy(); + auto& ctImpl = hostCt(ct); + setHostCt(ct, context->EvalChebyshevSeries(ctImpl, coeffs, a, b)); + return; +} + +Ciphertext<DCRTPoly> OpenFheEngine::rescale(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ciphertext); + return wrapHostCt(ctx, context->Rescale(ctImpl)); +} + +void OpenFheEngine::rescaleInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext) { + auto& context = hostContext(ctx); + ciphertext->EnsureLazyHostCopy(); + auto& ctImpl = hostCt(ciphertext); + setHostCt(ciphertext, context->Rescale(ctImpl)); + return; +} + +Ciphertext<DCRTPoly> OpenFheEngine::accumulateSum(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, int slots, int stride) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + + lbcrypto::Ciphertext<lbcrypto::DCRTPoly> result_ct = std::make_shared<lbcrypto::CiphertextImpl<lbcrypto::DCRTPoly>>(ctImpl); + + for (int i = 0; i < log2(slots); i++) { + int rot_idx = stride * (1 << i); + auto tmp = context->EvalRotate(result_ct, rot_idx); + context->EvalAddInPlace(result_ct, tmp); + } + + return wrapHostCt(ctx, result_ct); +} + +void OpenFheEngine::accumulateSumInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, int slots, int stride) { + auto& context = hostContext(ctx); + ct->EnsureLazyHostCopy(); + auto& ctImpl = hostCt(ct); + + for (int i = 0; i < log2(slots); i++) { + int rot_idx = stride * (1 << i); + auto tmp = context->EvalRotate(ctImpl, rot_idx); + context->EvalAddInPlace(ctImpl, tmp); + } + + return; +} + +void OpenFheEngine::accumulateSumInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, int slots, int stride, int start) { + auto& context = hostContext(ctx); + ct->EnsureLazyHostCopy(); + auto& ctImpl = hostCt(ct); + + for (int s = start; s < slots; s <<= 1) { + int rot_idx = stride * s; + auto tmp = context->EvalRotate(ctImpl, rot_idx); + context->EvalAddInPlace(ctImpl, tmp); + } + + return; +} + +BootstrapSetupPolicy OpenFheEngine::bootstrapSetupPolicy(bool /*precompute*/, bool /*btsfirstboot*/, int32_t modEvalLevels) const { + // Behaviour preserved verbatim from the previous CPU evalBootstrapSetup, which called the 6-arg + // EvalBootstrapSetup(levelBudget, dim1, slots, correctionFactor, /*precompute=*/true, modall). + // NOTE (pre-existing, flagged — not a behavior change in this refactor): in OpenFHE's single + // signature (..., precompute, BTSlotsEncoding, modevallevels=-1), that 6th `modall` argument lands + // in BTSlotsEncoding, NOT modevallevels — so the CPU "configure mod-eval levels" intent is not + // actually in effect (modevallevels stays -1). Reproduced exactly here; needs a separate decision. + return BootstrapSetupPolicy{ /*precompute=*/true, /*btSlotsEncoding=*/modEvalLevels != 0, /*modEvalLevels=*/-1 }; +} + +void OpenFheEngine::evalBootstrapKeyGen(CryptoContextImpl<DCRTPoly>& ctx, const PrivateKey<DCRTPoly>& secretKey, uint32_t slots) { + if (isContextLoaded()) { + OPENFHE_THROW("Context is already loaded"); + } + auto& skImpl = std::any_cast<const lbcrypto::PrivateKey<lbcrypto::DCRTPoly>&>(secretKey->pimpl); + + auto& context = hostContext(ctx); + context->EvalBootstrapKeyGen(skImpl, slots); + return; +} + +Ciphertext<DCRTPoly> +OpenFheEngine::evalBootstrap(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ciphertext); + return wrapHostCt(ctx, context->EvalBootstrap(ctImpl, numIterations, precision)); +} + +void OpenFheEngine::evalBootstrapInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled) { + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ciphertext); + ciphertext = wrapHostCt(ctx, context->EvalBootstrap(ctImpl, numIterations, precision)); +} + +void OpenFheEngine::recoverHostCiphertext(CryptoContextImpl<DCRTPoly>&, Ciphertext<DCRTPoly>&) { + // CPU backend: the ciphertext is never device-resident, so ct->host already holds the current + // value. Nothing to read back. +} + +void OpenFheEngine::convolutionTransformInPlace(CryptoContextImpl<DCRTPoly>& ctx, + Ciphertext<DCRTPoly>& ct, + int gStep, + int bStep, + const std::vector<Plaintext>& pts, + const std::vector<int>& indexes, + int stride, + int rowSize) { + OPENFHE_THROW("ConvolutionTransform: not implemented for CPU path"); +} + +void OpenFheEngine::specialConvolutionTransformInPlace(CryptoContextImpl<DCRTPoly>& ctx, + Ciphertext<DCRTPoly>& ct, + int gStep, + int bStep, + const std::vector<Plaintext>& pts, + Plaintext& mask, + const std::vector<int>& indexes, + int stride, + int maskRotationStride, + int rowSize) { + OPENFHE_THROW("SpecialConvolutionTransform: not implemented for CPU path"); +} + +// Loading objects to a device is a CUDA-only concept; on the CPU backend these are no-ops. +void OpenFheEngine::loadContext(CryptoContextImpl<DCRTPoly>&, const PublicKey<DCRTPoly>&) { +} + +void OpenFheEngine::loadPlaintext(CryptoContextImpl<DCRTPoly>&, Plaintext&) { +} + +void OpenFheEngine::loadCiphertext(CryptoContextImpl<DCRTPoly>&, Ciphertext<DCRTPoly>&) { +} + +std::shared_ptr<void> OpenFheEngine::evalFastRotationPrecompute(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct) { + + auto& context = hostContext(ctx); + auto& ctImpl = hostCt(ct); + return context->EvalFastRotationPrecompute(ctImpl); +} + +// ---- Ciphertext backend hooks ---- +// The CPU backend keeps no device-resident payload, so these operate on the host value via the value +// type's host primitives (the host computation lives there, once). + +std::any OpenFheEngine::cloneCiphertextBackend(CryptoContextImpl<DCRTPoly>&, const CiphertextImpl<DCRTPoly>&) { + return std::any{}; +} + +size_t OpenFheEngine::ciphertextLevel(CryptoContextImpl<DCRTPoly>&, const CiphertextImpl<DCRTPoly>& ct) { + return ct.GetLevelHost(); +} + +size_t OpenFheEngine::ciphertextNoiseScaleDeg(CryptoContextImpl<DCRTPoly>&, const CiphertextImpl<DCRTPoly>& ct) { + return ct.GetNoiseScaleDegHost(); +} + +void OpenFheEngine::setCiphertextSlots(CryptoContextImpl<DCRTPoly>&, CiphertextImpl<DCRTPoly>& ct, size_t slots) { + ct.SetSlotsHost(slots); +} + +void OpenFheEngine::setCiphertextLevel(CryptoContextImpl<DCRTPoly>&, CiphertextImpl<DCRTPoly>& ct, size_t level) { + ct.SetLevelHost(level); +} + +// ---- Context backend state ---- +// The CPU backend is never device-loaded and manages no devices. + +bool OpenFheEngine::isContextLoaded() const { + return false; +} + +void OpenFheEngine::synchronize() const { +} + +void OpenFheEngine::teardown() { +} + +void OpenFheEngine::setDevices(const std::vector<int>&) { +} + +std::vector<int> OpenFheEngine::devices() const { + return {}; +} + +} // namespace fideslib diff --git a/api/engine/cpu/OpenFheEngine.hpp b/api/engine/cpu/OpenFheEngine.hpp new file mode 100644 index 00000000..12895b1b --- /dev/null +++ b/api/engine/cpu/OpenFheEngine.hpp @@ -0,0 +1,110 @@ +#ifndef API_OPENFHEENGINE_HPP +#define API_OPENFHEENGINE_HPP + +#include "engine/Engine.hpp" + +namespace fideslib { + +/// @brief CPU backend. Every operation delegates to OpenFHE on the host. +/// All CPU operation code lives in OpenFheEngine.cpp. +class OpenFheEngine final : public Engine { + public: + const char* name() const override { + return "OpenFHE (CPU)"; + } + + Backend backend() const override { + return Backend::CPU; + } + + Ciphertext<DCRTPoly> evalNegate(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct) override; + void evalNegateInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) override; + Ciphertext<DCRTPoly> evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) override; + void evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) override; + Ciphertext<DCRTPoly> evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, Plaintext& pt) override; + Ciphertext<DCRTPoly> evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, double scalar) override; + void evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Plaintext& pt) override; + void evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) override; + Ciphertext<DCRTPoly> evalAddMany(CryptoContextImpl<DCRTPoly>& ctx, const std::vector<Ciphertext<DCRTPoly>>& ciphertexts) override; + void evalAddManyInPlace(CryptoContextImpl<DCRTPoly>& ctx, std::vector<Ciphertext<DCRTPoly>>& ciphertexts) override; + Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) override; + Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, Plaintext& pt) override; + Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, Plaintext& pt, const Ciphertext<DCRTPoly>& ct) override; + Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, double scalar) override; + Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, double scalar, const Ciphertext<DCRTPoly>& ct) override; + void evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) override; + void evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) override; + void evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, double scalar, Ciphertext<DCRTPoly>& ct1) override; + Ciphertext<DCRTPoly> evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) override; + Ciphertext<DCRTPoly> evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, Plaintext& pt) override; + Ciphertext<DCRTPoly> evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, double scalar) override; + void evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Plaintext& pt) override; + void evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) override; + void evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Ciphertext<DCRTPoly>& ct2) override; + Ciphertext<DCRTPoly> evalSquare(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct) override; + void evalSquareInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) override; + Ciphertext<DCRTPoly> evalRotate(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext, int32_t index) override; + void evalRotateInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext, int32_t index) override; + Ciphertext<DCRTPoly> + evalFastRotation(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const int32_t index, const uint32_t m, const std::shared_ptr<void>& precomp) override; + Ciphertext<DCRTPoly> + evalFastRotationExt(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const int32_t index, const std::shared_ptr<void>& digits, bool addFirst) override; + std::vector<Ciphertext<DCRTPoly>> evalFastRotation(CryptoContextImpl<DCRTPoly>& ctx, + const Ciphertext<DCRTPoly>& ct, + const std::vector<int32_t>& indices, + const uint32_t m, + const std::shared_ptr<void>& precomp) override; + std::vector<Ciphertext<DCRTPoly>> + evalFastRotationExt(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const std::vector<int32_t>& indices, const std::shared_ptr<void>& digits, bool addFirst) override; + Ciphertext<DCRTPoly> evalChebyshevSeries(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, std::vector<double>& coeffs, double a, double b) override; + void evalChebyshevSeriesInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, std::vector<double>& coeffs, double a, double b) override; + Ciphertext<DCRTPoly> rescale(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext) override; + void rescaleInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext) override; + Ciphertext<DCRTPoly> accumulateSum(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, int slots, int stride) override; + void accumulateSumInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, int slots, int stride) override; + void accumulateSumInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, int slots, int stride, int start) override; + BootstrapSetupPolicy bootstrapSetupPolicy(bool precompute, bool btsfirstboot, int32_t modEvalLevels) const override; + void evalBootstrapKeyGen(CryptoContextImpl<DCRTPoly>& ctx, const PrivateKey<DCRTPoly>& secretKey, uint32_t slots) override; + Ciphertext<DCRTPoly> + evalBootstrap(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled) override; + void evalBootstrapInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled) override; + void recoverHostCiphertext(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) override; + void convolutionTransformInPlace(CryptoContextImpl<DCRTPoly>& ctx, + Ciphertext<DCRTPoly>& ct, + int gStep, + int bStep, + const std::vector<Plaintext>& pts, + const std::vector<int>& indexes, + int stride, + int rowSize) override; + void specialConvolutionTransformInPlace(CryptoContextImpl<DCRTPoly>& ctx, + Ciphertext<DCRTPoly>& ct, + int gStep, + int bStep, + const std::vector<Plaintext>& pts, + Plaintext& mask, + const std::vector<int>& indexes, + int stride, + int maskRotationStride, + int rowSize) override; + void loadContext(CryptoContextImpl<DCRTPoly>& ctx, const PublicKey<DCRTPoly>& publicKey) override; + void loadPlaintext(CryptoContextImpl<DCRTPoly>& ctx, Plaintext& pt) override; + void loadCiphertext(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) override; + std::shared_ptr<void> evalFastRotationPrecompute(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct) override; + + std::any cloneCiphertextBackend(CryptoContextImpl<DCRTPoly>& ctx, const CiphertextImpl<DCRTPoly>& src) override; + size_t ciphertextLevel(CryptoContextImpl<DCRTPoly>& ctx, const CiphertextImpl<DCRTPoly>& ct) override; + size_t ciphertextNoiseScaleDeg(CryptoContextImpl<DCRTPoly>& ctx, const CiphertextImpl<DCRTPoly>& ct) override; + void setCiphertextSlots(CryptoContextImpl<DCRTPoly>& ctx, CiphertextImpl<DCRTPoly>& ct, size_t slots) override; + void setCiphertextLevel(CryptoContextImpl<DCRTPoly>& ctx, CiphertextImpl<DCRTPoly>& ct, size_t level) override; + + bool isContextLoaded() const override; + void synchronize() const override; + void teardown() override; + void setDevices(const std::vector<int>& devices) override; + std::vector<int> devices() const override; +}; + +} // namespace fideslib + +#endif diff --git a/api/engine/cuda/CudaEngine.cpp b/api/engine/cuda/CudaEngine.cpp new file mode 100644 index 00000000..e34b2f26 --- /dev/null +++ b/api/engine/cuda/CudaEngine.cpp @@ -0,0 +1,774 @@ +#include "engine/cuda/CudaEngine.hpp" + +#include "CryptoContext.hpp" +#include "engine/EngineCommon.hpp" + +// GPU CKKS implementation headers for the migrated device-side ops. +#include "CKKS/AccumulateBroadcast.cuh" +#include "CKKS/ApproxModEval.cuh" +#include "CKKS/Bootstrap.cuh" +#include "CKKS/Ciphertext.cuh" +#include "CKKS/Context.cuh" +#include "CKKS/KeySwitchingKey.cuh" +#include "CKKS/LinearTransform.cuh" +#include "CKKS/Parameters.cuh" +#include "CKKS/Plaintext.cuh" +#include "CKKS/forwardDefs.cuh" +#include "CKKS/openfhe-interface/RawCiphertext.cuh" +#include "CudaUtils.cuh" + +#include <any> +#include <memory> +#include <openfhe.h> // OPENFHE_THROW in shared precondition checks + +namespace fideslib { + +namespace { +// The neutral value types store the GPU-resident payload as a shared_ptr inside their `device` slot +// (the device object is not copyable). These unwrap it; an empty slot means "not resident", which the +// engine ensures via LoadCiphertext/LoadPlaintext before every use. +std::shared_ptr<FIDESlib::CKKS::Ciphertext> deviceCt(const Ciphertext<DCRTPoly>& ct) { + return std::any_cast<std::shared_ptr<FIDESlib::CKKS::Ciphertext>>(ct->device); +} + +std::shared_ptr<FIDESlib::CKKS::Plaintext> devicePt(const Plaintext& pt) { + return std::any_cast<std::shared_ptr<FIDESlib::CKKS::Plaintext>>(pt->device); +} +} // namespace + +// CKKS prime tables for the GPU context parameters (used by loadContext). +static std::vector<FIDESlib::PrimeRecord> p64{ { .p = 2305843009218281473 }, + { .p = 2251799661248513 }, + { .p = 2251799661641729 }, + { .p = 2251799665180673 }, + { .p = 2251799682088961 }, + { .p = 2251799678943233 }, + { .p = 2251799717609473 }, + { .p = 2251799710138369 }, + { .p = 2251799708827649 }, + { .p = 2251799707385857 }, + { .p = 2251799713677313 }, + { .p = 2251799712366593 }, + { .p = 2251799716691969 }, + { .p = 2251799714856961 }, + { .p = 2251799726522369 }, + { .p = 2251799726129153 }, + { .p = 2251799747493889 }, + { .p = 2251799741857793 }, + { .p = 2251799740416001 }, + { .p = 2251799746707457 }, + { .p = 2251799756013569 }, + { .p = 2251799775805441 }, + { .p = 2251799763091457 }, + { .p = 2251799767154689 }, + { .p = 2251799765975041 }, + { .p = 2251799770562561 }, + { .p = 2251799769776129 }, + { .p = 2251799772266497 }, + { .p = 2251799775281153 }, + { .p = 2251799774887937 }, + { .p = 2251799797432321 }, + { .p = 2251799787995137 }, + { .p = 2251799787601921 }, + { .p = 2251799791403009 }, + { .p = 2251799789568001 }, + { .p = 2251799795466241 }, + { .p = 2251799807131649 }, + { .p = 2251799806345217 }, + { .p = 2251799805165569 }, + { .p = 2251799813554177 }, + { .p = 2251799809884161 }, + { .p = 2251799810670593 }, + { .p = 2251799818928129 }, + { .p = 2251799816568833 }, + { .p = 2251799815520257 } }; + +static std::vector<FIDESlib::PrimeRecord> sp64{ { .p = 2305843009218936833 }, + { .p = 2305843009220116481 }, + { .p = 2305843009221820417 }, + { .p = 2305843009224179713 }, + { .p = 2305843009225228289 }, + { .p = 2305843009227980801 }, + { .p = 2305843009229160449 }, + { .p = 2305843009229946881 }, + { .p = 2305843009231650817 }, + { .p = 2305843009235189761 }, + { .p = 2305843009240301569 }, + { .p = 2305843009242923009 }, + { .p = 2305843009244889089 }, + { .p = 2305843009245413377 }, + { .p = 2305843009247641601 } }; + +Ciphertext<DCRTPoly> CudaEngine::evalNegate(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + res_gpu->multScalar(-1.0); + return result; +} + +void CudaEngine::evalNegateInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) { + ctx.LoadCiphertext(ct); + auto ct_gpu = deviceCt(ct); + ct_gpu->multScalar(-1.0); +} + +Ciphertext<DCRTPoly> CudaEngine::evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct1)); + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct2)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct1); + auto res_gpu = deviceCt(result); + auto ct2_gpu = deviceCt(ct2); + res_gpu->add(*ct2_gpu); + return result; +} + +void CudaEngine::evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) { + ctx.LoadCiphertext(ct1); + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct2)); + auto res_gpu = deviceCt(ct1); + auto ct2_gpu = deviceCt(ct2); + res_gpu->add(*ct2_gpu); +} + +Ciphertext<DCRTPoly> CudaEngine::evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, Plaintext& pt) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + ctx.LoadPlaintext(pt); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + auto pt_gpu = devicePt(pt); + res_gpu->addPt(*pt_gpu); + return result; +} + +Ciphertext<DCRTPoly> CudaEngine::evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, double scalar) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + res_gpu->addScalar(scalar); + return result; +} + +void CudaEngine::evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Plaintext& pt) { + ctx.LoadCiphertext(ct1); + ctx.LoadPlaintext(pt); + auto res_gpu = deviceCt(ct1); + auto pt_gpu = devicePt(pt); + res_gpu->addPt(*pt_gpu); +} + +void CudaEngine::evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) { + ctx.LoadCiphertext(ct1); + auto res_gpu = deviceCt(ct1); + res_gpu->addScalar(scalar); +} + +Ciphertext<DCRTPoly> CudaEngine::evalAddMany(CryptoContextImpl<DCRTPoly>& ctx, const std::vector<Ciphertext<DCRTPoly>>& ciphertexts) { + if (ciphertexts.empty()) { + OPENFHE_THROW("EvalAddMany: input ciphertext vector is empty"); + } + + for (const auto& ct : ciphertexts) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + } + + const size_t inSize = ciphertexts.size(); + const size_t lim = inSize * 2 - 2; + std::vector<Ciphertext<DCRTPoly>> ciphertextSumVec; + ciphertextSumVec.resize(inSize - 1); + size_t ctrIndex = 0; + + for (size_t i = 0; i < lim; i = i + 2) { + ciphertextSumVec[ctrIndex++] = + ctx.EvalAdd(i < inSize ? ciphertexts[i] : ciphertextSumVec[i - inSize], i + 1 < inSize ? ciphertexts[i + 1] : ciphertextSumVec[i + 1 - inSize]); + } + + return ciphertextSumVec.back(); +} + +void CudaEngine::evalAddManyInPlace(CryptoContextImpl<DCRTPoly>& ctx, std::vector<Ciphertext<DCRTPoly>>& ciphertexts) { + if (ciphertexts.empty()) { + OPENFHE_THROW("EvalAddManyInPlace: input ciphertext vector is empty"); + } + + for (const auto& ct : ciphertexts) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + } + + for (size_t j = 1; j < ciphertexts.size(); j = j * 2) { + for (size_t i = 0; i < ciphertexts.size(); i = i + 2 * j) { + if ((i + j) < ciphertexts.size()) { + if (ciphertexts[i] != nullptr && ciphertexts[i + j] != nullptr) { + ctx.EvalAddInPlace(ciphertexts[i], ciphertexts[i + j]); + } else if (ciphertexts[i] == nullptr && ciphertexts[i + j] != nullptr) { + ciphertexts[i] = ciphertexts[i + j]; + } + } + } + } +} + +Ciphertext<DCRTPoly> CudaEngine::evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct1)); + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct2)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct1); + auto res_gpu = deviceCt(result); + auto ct2_gpu = deviceCt(ct2); + res_gpu->sub(*ct2_gpu); + return result; +} + +Ciphertext<DCRTPoly> CudaEngine::evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, Plaintext& pt) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + ctx.LoadPlaintext(pt); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + auto pt_gpu = devicePt(pt); + res_gpu->subPt(*pt_gpu); + return result; +} + +Ciphertext<DCRTPoly> CudaEngine::evalSub(CryptoContextImpl<DCRTPoly>& ctx, Plaintext& pt, const Ciphertext<DCRTPoly>& ct) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + ctx.LoadPlaintext(pt); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + auto pt_gpu = devicePt(pt); + res_gpu->multScalar(-1.0); + res_gpu->addPt(*pt_gpu); + return result; +} + +Ciphertext<DCRTPoly> CudaEngine::evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, double scalar) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + res_gpu->addScalar(-scalar); + return result; +} + +Ciphertext<DCRTPoly> CudaEngine::evalSub(CryptoContextImpl<DCRTPoly>& ctx, double scalar, const Ciphertext<DCRTPoly>& ct) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + res_gpu->multScalar(-1.0); + res_gpu->addScalar(scalar); + res_gpu->multScalar(-1.0); + return result; +} + +void CudaEngine::evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) { + ctx.LoadCiphertext(ct1); + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct2)); + auto res_gpu = deviceCt(ct1); + auto ct2_gpu = deviceCt(ct2); + res_gpu->sub(*ct2_gpu); +} + +void CudaEngine::evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) { + ctx.LoadCiphertext(ct1); + auto res_gpu = deviceCt(ct1); + res_gpu->addScalar(-scalar); +} + +void CudaEngine::evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, double scalar, Ciphertext<DCRTPoly>& ct1) { + ctx.LoadCiphertext(ct1); + auto res_gpu = deviceCt(ct1); + res_gpu->multScalar(-1.0); + res_gpu->addScalar(scalar); + res_gpu->multScalar(-1.0); +} + +Ciphertext<DCRTPoly> CudaEngine::evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct1)); + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct2)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct1); + auto res_gpu = deviceCt(result); + auto ct2_gpu = deviceCt(ct2); + res_gpu->mult(*ct2_gpu); + return result; +} + +Ciphertext<DCRTPoly> CudaEngine::evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, Plaintext& pt) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct1)); + ctx.LoadPlaintext(pt); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct1); + auto res_gpu = deviceCt(result); + auto pt_gpu = devicePt(pt); + res_gpu->multPt(*pt_gpu); + return result; +} + +Ciphertext<DCRTPoly> CudaEngine::evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, double scalar) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct1)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct1); + auto res_gpu = deviceCt(result); + res_gpu->multScalar(scalar); + return result; +} + +void CudaEngine::evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Plaintext& pt) { + ctx.LoadCiphertext(ct1); + ctx.LoadPlaintext(pt); + auto res_gpu = deviceCt(ct1); + auto pt_gpu = devicePt(pt); + res_gpu->multPt(*pt_gpu); +} + +void CudaEngine::evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) { + ctx.LoadCiphertext(ct1); + auto res_gpu = deviceCt(ct1); + res_gpu->multScalar(scalar); +} + +void CudaEngine::evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Ciphertext<DCRTPoly>& ct2) { + ctx.LoadCiphertext(ct1); + ctx.LoadCiphertext(ct2); + auto res_gpu = deviceCt(ct1); + auto ct2_gpu = deviceCt(ct2); + res_gpu->mult(*ct2_gpu); +} + +Ciphertext<DCRTPoly> CudaEngine::evalSquare(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + res_gpu->square(); + return result; +} + +void CudaEngine::evalSquareInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) { + ctx.LoadCiphertext(ct); + auto ct_gpu = deviceCt(ct); + ct_gpu->square(); +} + +Ciphertext<DCRTPoly> CudaEngine::evalRotate(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext, int32_t index) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ciphertext)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ciphertext); + auto res_gpu = deviceCt(result); + res_gpu->rotate(index); + return result; +} + +void CudaEngine::evalRotateInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext, int32_t index) { + ctx.LoadCiphertext(ciphertext); + auto ct_gpu = deviceCt(ciphertext); + ct_gpu->rotate(index); +} + +Ciphertext<DCRTPoly> +CudaEngine::evalFastRotation(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const int32_t index, const uint32_t m, const std::shared_ptr<void>& precomp) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + auto ct_gpu = deviceCt(ct); + res_gpu->copy(*ct_gpu); + res_gpu->rotate((int)index, true); + + return result; +} + +Ciphertext<DCRTPoly> +CudaEngine::evalFastRotationExt(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const int32_t index, const std::shared_ptr<void>& digits, bool addFirst) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + auto ct_gpu = deviceCt(ct); + // ct_gpu->rotate((int)index, false); + res_gpu->copy(*ct_gpu); + res_gpu->rotate((int)index, false); + + return result; +} + +std::vector<Ciphertext<DCRTPoly>> CudaEngine::evalFastRotation(CryptoContextImpl<DCRTPoly>& ctx, + const Ciphertext<DCRTPoly>& ct, + const std::vector<int32_t>& indices, + const uint32_t m, + const std::shared_ptr<void>& precomp) { + std::vector<Ciphertext<DCRTPoly>> results; + + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + auto ct_gpu = deviceCt(ct); + + // Create result ciphertexts. + std::vector<FIDESlib::CKKS::Ciphertext*> results_gpu; + std::vector<int32_t> indices_real; + for (int indice : indices) { + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + + if (indice != 0) { + indices_real.push_back(indice); + auto res_gpu = deviceCt(result); + results_gpu.push_back(res_gpu.get()); + } + results.push_back(result); + } + + ct_gpu->rotate_hoisted(indices_real, results_gpu, false); + return results; +} + +std::vector<Ciphertext<DCRTPoly>> +CudaEngine::evalFastRotationExt(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const std::vector<int32_t>& indices, const std::shared_ptr<void>& digits, bool addFirst) { + std::vector<Ciphertext<DCRTPoly>> results; + + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + auto ct_gpu = deviceCt(ct); + + std::vector<FIDESlib::CKKS::Ciphertext*> results_gpu; + std::vector<int32_t> indices_real; + for (int indice : indices) { + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + + if (indice != 0) { + indices_real.push_back(indice); + auto res_gpu = deviceCt(result); + results_gpu.push_back(res_gpu.get()); + } + results.push_back(result); + } + + ct_gpu->rotate_hoisted(indices_real, results_gpu, true); + return results; +} + +Ciphertext<DCRTPoly> CudaEngine::evalChebyshevSeries(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, std::vector<double>& coeffs, double a, double b) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + FIDESlib::CKKS::evalChebyshevSeries(*res_gpu, coeffs, a, b); + return result; +} + +void CudaEngine::evalChebyshevSeriesInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, std::vector<double>& coeffs, double a, double b) { + ctx.LoadCiphertext(ct); + auto res_gpu = deviceCt(ct); + FIDESlib::CKKS::evalChebyshevSeries(*res_gpu, coeffs, a, b); +} + +Ciphertext<DCRTPoly> CudaEngine::rescale(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ciphertext)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ciphertext); + auto res_gpu = deviceCt(result); + res_gpu->rescale(); + return result; +} + +void CudaEngine::rescaleInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext) { + ctx.LoadCiphertext(ciphertext); + auto res_gpu = deviceCt(ciphertext); + res_gpu->rescale(); +} + +Ciphertext<DCRTPoly> CudaEngine::accumulateSum(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, int slots, int stride) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ct)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ct); + auto res_gpu = deviceCt(result); + FIDESlib::CKKS::Accumulate(*res_gpu, 4, stride, slots); + return result; +} + +void CudaEngine::accumulateSumInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, int slots, int stride) { + ctx.LoadCiphertext(ct); + auto res_gpu = deviceCt(ct); + FIDESlib::CKKS::Accumulate(*res_gpu, 4, stride, slots); +} + +void CudaEngine::accumulateSumInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, int slots, int stride, int start) { + ctx.LoadCiphertext(ct); + auto res_gpu = deviceCt(ct); + FIDESlib::CKKS::Accumulate(*res_gpu, 4, stride, slots, start); +} + +BootstrapSetupPolicy CudaEngine::bootstrapSetupPolicy(bool precompute, bool btsfirstboot, int32_t modEvalLevels) const { + // The GPU path forwards the caller's precompute/firstboot and the mod-eval levels unchanged + // (matching the previous 7-arg EvalBootstrapSetup(..., precompute, btsfirstboot, modall) call). + return BootstrapSetupPolicy{ precompute, btsfirstboot, modEvalLevels }; +} + +void CudaEngine::evalBootstrapKeyGen(CryptoContextImpl<DCRTPoly>& ctx, const PrivateKey<DCRTPoly>& secretKey, uint32_t slots) { + if (isContextLoaded()) { + OPENFHE_THROW("Context is already loaded"); + } + auto& skImpl = std::any_cast<const lbcrypto::PrivateKey<lbcrypto::DCRTPoly>&>(secretKey->pimpl); + + ctx.slots_bootstrap.push_back(slots); + FIDESlib::CKKS::GenBootstrapKeys(skImpl, slots, ctx.keyDist == fideslib::SPARSE_ENCAPSULATED); +} + +Ciphertext<DCRTPoly> +CudaEngine::evalBootstrap(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ciphertext)); + Ciphertext<DCRTPoly> result = std::make_shared<CiphertextImpl<DCRTPoly>>(*ciphertext); + auto res_gpu = deviceCt(result); + FIDESlib::CKKS::Bootstrap(*res_gpu, res_gpu->slots, prescaled); + return result; +} + +void CudaEngine::evalBootstrapInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled) { + ctx.LoadCiphertext(const_cast<Ciphertext<DCRTPoly>&>(ciphertext)); + auto res_gpu = deviceCt(ciphertext); + FIDESlib::CKKS::Bootstrap(*res_gpu, res_gpu->slots, prescaled); +} + +void CudaEngine::recoverHostCiphertext(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) { + if (!ct->device.has_value()) { + // Already resident on the host; nothing to read back. + return; + } + ct->EnsureLazyHostCopy(); + auto& ct_cpu = std::any_cast<lbcrypto::Ciphertext<lbcrypto::DCRTPoly>&>(ct->host); + + auto ct_gpu = deviceCt(ct); + FIDESlib::CKKS::RawCipherText raw_ct; + ct_gpu->store(raw_ct); + + // Grow the host shadow if the device result carries more RNS limbs than it currently holds + // (e.g. after a level-raising op like ModRaise). GetOpenFHECipherText overwrites the limb values + // and trims down to the device limb count, so a zero ciphertext at the full level is a valid + // container — built directly from the crypto parameters, no secret key needed. + size_t cpu_levels = ct_cpu->GetElements()[0].GetAllElements().size(); + size_t gpu_levels = raw_ct.numRes; + if (cpu_levels < gpu_levels) { + auto& context = std::any_cast<lbcrypto::CryptoContext<lbcrypto::DCRTPoly>&>(ctx.host); + const auto elemParams = context->GetCryptoParameters()->GetElementParams(); + lbcrypto::DCRTPoly zero(elemParams, Format::EVALUATION, true); + auto fresh = std::make_shared<lbcrypto::CiphertextImpl<lbcrypto::DCRTPoly>>(*ct_cpu); + fresh->SetElements({ zero, zero }); + ct_cpu = fresh; + } + + // Overwrite the host ciphertext with the device data. + FIDESlib::CKKS::GetOpenFHECipherText(ct_cpu, raw_ct); +} + +void CudaEngine::convolutionTransformInPlace(CryptoContextImpl<DCRTPoly>& ctx, + Ciphertext<DCRTPoly>& ct, + int gStep, + int bStep, + const std::vector<Plaintext>& pts, + const std::vector<int>& indexes, + int stride, + int rowSize) { + ctx.LoadCiphertext(ct); + auto ct_gpu = deviceCt(ct); + std::vector<FIDESlib::CKKS::Plaintext*> pts_gpu; + pts_gpu.reserve(pts.size()); + for (const auto& pt : pts) { + ctx.LoadPlaintext(const_cast<Plaintext&>(pt)); + auto pt_gpu = devicePt(pt); + pts_gpu.push_back(pt_gpu.get()); + } + + if (rowSize == 0) { + rowSize = bStep * gStep; + } + + FIDESlib::CKKS::ConvolutionTransform(*ct_gpu, rowSize, bStep, pts_gpu, stride, indexes, gStep); +} + +void CudaEngine::specialConvolutionTransformInPlace(CryptoContextImpl<DCRTPoly>& ctx, + Ciphertext<DCRTPoly>& ct, + int gStep, + int bStep, + const std::vector<Plaintext>& pts, + Plaintext& mask, + const std::vector<int>& indexes, + int stride, + int maskRotationStride, + int rowSize) { + ctx.LoadCiphertext(ct); + auto ct_gpu = deviceCt(ct); + std::vector<FIDESlib::CKKS::Plaintext*> pts_gpu; + pts_gpu.reserve(pts.size()); + for (const auto& pt : pts) { + ctx.LoadPlaintext(const_cast<Plaintext&>(pt)); + auto pt_gpu = devicePt(pt); + pts_gpu.push_back(pt_gpu.get()); + } + + // Load mask + ctx.LoadPlaintext(mask); + auto mask_gpu = devicePt(mask); + + if (rowSize == 0) { + rowSize = bStep * gStep; + } + + FIDESlib::CKKS::SpecialConvolutionTransform(*ct_gpu, rowSize, bStep, pts_gpu, *mask_gpu, stride, maskRotationStride, indexes, gStep); +} + +void CudaEngine::loadContext(CryptoContextImpl<DCRTPoly>& ctx, const PublicKey<DCRTPoly>& publicKey) { + if (isContextLoaded()) + return; + + auto& context = std::any_cast<lbcrypto::CryptoContext<lbcrypto::DCRTPoly>&>(ctx.host); + FIDESlib::CKKS::Parameters params{ .logN = 16, .L = 6, .dnum = 2, .primes = std::vector(p64), .Sprimes = std::vector(sp64), .batch = 100 }; + + // Determine the boot configuration based on the secret key distribution. + const auto cryptoParams = std::dynamic_pointer_cast<lbcrypto::CryptoParametersCKKSRNS>(context->GetCryptoParameters()); + FIDESlib::BOOT_CONFIG bootConfig; + switch (ctx.keyDist) { + case fideslib::UNIFORM_TERNARY: bootConfig = FIDESlib::UNIFORM; break; + case fideslib::SPARSE_TERNARY: bootConfig = FIDESlib::SPARSE; break; + case fideslib::SPARSE_ENCAPSULATED: bootConfig = FIDESlib::ENCAPS; break; + default: bootConfig = FIDESlib::UNIFORM; break; + } + + FIDESlib::CKKS::RawParams rawParams = FIDESlib::CKKS::GetRawParams(context, bootConfig); + params = params.adaptTo(rawParams); + FIDESlib::CKKS::Context c = FIDESlib::CKKS::GenCryptoContextGPU(params, devices_); + + auto& pkImpl = std::any_cast<const lbcrypto::PublicKey<lbcrypto::DCRTPoly>&>(publicKey->pimpl); + + // Multiplicative key switching key. + auto& keyMap = context->GetAllEvalMultKeys(); // lbcrypto::CryptoContextImpl<lbcrypto::DCRTPoly>::s_evalMultKeyMap; + if (keyMap.find(pkImpl->GetKeyTag()) != keyMap.end()) { + auto raw_eval_ksk = FIDESlib::CKKS::GetEvalKeySwitchKey(pkImpl); + FIDESlib::CKKS::KeySwitchingKey eval_ksk(c); + eval_ksk.Initialize(raw_eval_ksk); + c->AddEvalKey(std::move(eval_ksk)); + } + // Rotational key switching keys. + for (const auto& step : ctx.rotation_indexes) { + auto raw_rot_ksk = FIDESlib::CKKS::GetRotationKeySwitchKey(pkImpl, step); + FIDESlib::CKKS::KeySwitchingKey rot_ksk(c); + rot_ksk.Initialize(raw_rot_ksk); + c->AddRotationKey(step, std::move(rot_ksk)); + } + // Bootstrapping keys. + for (const auto& slot : ctx.slots_bootstrap) { + FIDESlib::CKKS::AddBootstrapPrecomputation(pkImpl, slot, c); + } + + context_ = std::make_unique<FIDESlib::CKKS::Context>(std::move(c)); +} + +void CudaEngine::loadPlaintext(CryptoContextImpl<DCRTPoly>& ctx, Plaintext& pt) { + if (pt->device.has_value()) + return; + + if (!isContextLoaded()) { + OPENFHE_THROW("CryptoContext not loaded to any device"); + } + + auto& context_gpu = *context_; + auto& context = std::any_cast<lbcrypto::CryptoContext<lbcrypto::DCRTPoly>&>(ctx.host); + const auto& ptImpl = std::any_cast<const lbcrypto::Plaintext&>(pt->host); + FIDESlib::CKKS::RawPlainText raw_pt = FIDESlib::CKKS::GetRawPlainText(context, ptImpl); + std::shared_ptr<FIDESlib::CKKS::Plaintext> gpu_pt = std::make_shared<FIDESlib::CKKS::Plaintext>(context_gpu, raw_pt); + pt->device = std::make_any<std::shared_ptr<FIDESlib::CKKS::Plaintext>>(std::move(gpu_pt)); +} + +void CudaEngine::loadCiphertext(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) { + if (ct->device.has_value()) + return; + + if (!isContextLoaded()) { + OPENFHE_THROW("CryptoContext not loaded to any device"); + } + + auto& context_gpu = *context_; + auto& context = std::any_cast<lbcrypto::CryptoContext<lbcrypto::DCRTPoly>&>(ctx.host); + const auto& ctImpl = std::any_cast<const lbcrypto::Ciphertext<lbcrypto::DCRTPoly>&>(ct->host); + FIDESlib::CKKS::RawCipherText raw_ct = FIDESlib::CKKS::GetRawCipherText(context, ctImpl); + std::shared_ptr<FIDESlib::CKKS::Ciphertext> gpu_ct = std::make_shared<FIDESlib::CKKS::Ciphertext>(context_gpu, raw_ct); + ct->device = std::make_any<std::shared_ptr<FIDESlib::CKKS::Ciphertext>>(std::move(gpu_ct)); +} + +std::shared_ptr<void> CudaEngine::evalFastRotationPrecompute(CryptoContextImpl<DCRTPoly>&, const Ciphertext<DCRTPoly>&) { + // GPU key switching does not use a precompute handle. + return nullptr; +} + +// ---- Ciphertext backend hooks ---- + +std::any CudaEngine::cloneCiphertextBackend(CryptoContextImpl<DCRTPoly>&, const CiphertextImpl<DCRTPoly>& src) { + // Not device-resident → nothing to clone (the host value is copied by the value type itself). + if (!src.device.has_value()) { + return std::any{}; + } + // Deep-copy the device payload into a fresh device ciphertext. + auto src_gpu = std::any_cast<std::shared_ptr<FIDESlib::CKKS::Ciphertext>>(src.device); + auto new_ct = std::make_shared<FIDESlib::CKKS::Ciphertext>(*context_); + new_ct->copy(*src_gpu); + return std::make_any<std::shared_ptr<FIDESlib::CKKS::Ciphertext>>(std::move(new_ct)); +} + +size_t CudaEngine::ciphertextLevel(CryptoContextImpl<DCRTPoly>& ctx, const CiphertextImpl<DCRTPoly>& ct) { + if (!ct.device.has_value()) { + return ct.GetLevelHost(); + } + // Depth is reversed in FIDESlib, so level = maxDepth - deviceLevel. + auto ct_gpu = std::any_cast<std::shared_ptr<FIDESlib::CKKS::Ciphertext>>(ct.device); + return ctx.multiplicative_depth - ct_gpu->getLevel(); +} + +size_t CudaEngine::ciphertextNoiseScaleDeg(CryptoContextImpl<DCRTPoly>&, const CiphertextImpl<DCRTPoly>& ct) { + if (!ct.device.has_value()) { + return ct.GetNoiseScaleDegHost(); + } + auto ct_gpu = std::any_cast<std::shared_ptr<FIDESlib::CKKS::Ciphertext>>(ct.device); + return ct_gpu->NoiseLevel; +} + +void CudaEngine::setCiphertextSlots(CryptoContextImpl<DCRTPoly>&, CiphertextImpl<DCRTPoly>& ct, size_t slots) { + if (!ct.device.has_value()) { + ct.SetSlotsHost(slots); + return; + } + auto ct_gpu = std::any_cast<std::shared_ptr<FIDESlib::CKKS::Ciphertext>>(ct.device); + ct_gpu->slots = static_cast<int>(slots); +} + +void CudaEngine::setCiphertextLevel(CryptoContextImpl<DCRTPoly>& ctx, CiphertextImpl<DCRTPoly>& ct, size_t level) { + if (!ct.device.has_value()) { + ct.SetLevelHost(level); + return; + } + auto ct_gpu = std::any_cast<std::shared_ptr<FIDESlib::CKKS::Ciphertext>>(ct.device); + ct_gpu->dropToLevel(ctx.multiplicative_depth - level); +} + +// ---- Context backend state ---- + +bool CudaEngine::isContextLoaded() const { + return context_ != nullptr; +} + +void CudaEngine::synchronize() const { + if (!context_) { + return; + } + for (const auto& device : devices_) { + cudaSetDevice(device); + cudaDeviceSynchronize(); + CudaCheckErrorModNoSync; + } +} + +void CudaEngine::teardown() { + if (!context_) { + return; + } + FIDESlib::CKKS::DeregisterCryptoContextGPU(*context_); + context_.reset(); +} + +void CudaEngine::setDevices(const std::vector<int>& devices) { + devices_ = devices; +} + +std::vector<int> CudaEngine::devices() const { + return devices_; +} + +CudaEngine::~CudaEngine() { + teardown(); +} + +} // namespace fideslib diff --git a/api/engine/cuda/CudaEngine.hpp b/api/engine/cuda/CudaEngine.hpp new file mode 100644 index 00000000..0ff36e35 --- /dev/null +++ b/api/engine/cuda/CudaEngine.hpp @@ -0,0 +1,125 @@ +#ifndef API_CUDAENGINE_HPP +#define API_CUDAENGINE_HPP + +#include "engine/Engine.hpp" + +#include "CKKS/forwardDefs.cuh" // FIDESlib::CKKS::Context (= shared_ptr<ContextData>) + +#include <memory> +#include <vector> + +namespace fideslib { + +/// @brief CUDA backend. Every operation runs FIDESlib's CUDA CKKS layer. +/// All CUDA operation code lives in CudaEngine.cpp. +class CudaEngine final : public Engine { + public: + const char* name() const override { + return "FIDESlib (CUDA)"; + } + + Backend backend() const override { + return Backend::CUDA; + } + + Ciphertext<DCRTPoly> evalNegate(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct) override; + void evalNegateInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) override; + Ciphertext<DCRTPoly> evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) override; + void evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) override; + Ciphertext<DCRTPoly> evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, Plaintext& pt) override; + Ciphertext<DCRTPoly> evalAdd(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, double scalar) override; + void evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Plaintext& pt) override; + void evalAddInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) override; + Ciphertext<DCRTPoly> evalAddMany(CryptoContextImpl<DCRTPoly>& ctx, const std::vector<Ciphertext<DCRTPoly>>& ciphertexts) override; + void evalAddManyInPlace(CryptoContextImpl<DCRTPoly>& ctx, std::vector<Ciphertext<DCRTPoly>>& ciphertexts) override; + Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) override; + Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, Plaintext& pt) override; + Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, Plaintext& pt, const Ciphertext<DCRTPoly>& ct) override; + Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, double scalar) override; + Ciphertext<DCRTPoly> evalSub(CryptoContextImpl<DCRTPoly>& ctx, double scalar, const Ciphertext<DCRTPoly>& ct) override; + void evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) override; + void evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) override; + void evalSubInPlace(CryptoContextImpl<DCRTPoly>& ctx, double scalar, Ciphertext<DCRTPoly>& ct1) override; + Ciphertext<DCRTPoly> evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, const Ciphertext<DCRTPoly>& ct2) override; + Ciphertext<DCRTPoly> evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, Plaintext& pt) override; + Ciphertext<DCRTPoly> evalMult(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct1, double scalar) override; + void evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Plaintext& pt) override; + void evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, double scalar) override; + void evalMultInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct1, Ciphertext<DCRTPoly>& ct2) override; + Ciphertext<DCRTPoly> evalSquare(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct) override; + void evalSquareInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) override; + Ciphertext<DCRTPoly> evalRotate(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext, int32_t index) override; + void evalRotateInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext, int32_t index) override; + Ciphertext<DCRTPoly> + evalFastRotation(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const int32_t index, const uint32_t m, const std::shared_ptr<void>& precomp) override; + Ciphertext<DCRTPoly> + evalFastRotationExt(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const int32_t index, const std::shared_ptr<void>& digits, bool addFirst) override; + std::vector<Ciphertext<DCRTPoly>> evalFastRotation(CryptoContextImpl<DCRTPoly>& ctx, + const Ciphertext<DCRTPoly>& ct, + const std::vector<int32_t>& indices, + const uint32_t m, + const std::shared_ptr<void>& precomp) override; + std::vector<Ciphertext<DCRTPoly>> + evalFastRotationExt(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, const std::vector<int32_t>& indices, const std::shared_ptr<void>& digits, bool addFirst) override; + Ciphertext<DCRTPoly> evalChebyshevSeries(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, std::vector<double>& coeffs, double a, double b) override; + void evalChebyshevSeriesInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, std::vector<double>& coeffs, double a, double b) override; + Ciphertext<DCRTPoly> rescale(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext) override; + void rescaleInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext) override; + Ciphertext<DCRTPoly> accumulateSum(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct, int slots, int stride) override; + void accumulateSumInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, int slots, int stride) override; + void accumulateSumInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct, int slots, int stride, int start) override; + BootstrapSetupPolicy bootstrapSetupPolicy(bool precompute, bool btsfirstboot, int32_t modEvalLevels) const override; + void evalBootstrapKeyGen(CryptoContextImpl<DCRTPoly>& ctx, const PrivateKey<DCRTPoly>& secretKey, uint32_t slots) override; + Ciphertext<DCRTPoly> + evalBootstrap(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled) override; + void evalBootstrapInPlace(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ciphertext, uint32_t numIterations, uint32_t precision, bool prescaled) override; + void recoverHostCiphertext(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) override; + void convolutionTransformInPlace(CryptoContextImpl<DCRTPoly>& ctx, + Ciphertext<DCRTPoly>& ct, + int gStep, + int bStep, + const std::vector<Plaintext>& pts, + const std::vector<int>& indexes, + int stride, + int rowSize) override; + void specialConvolutionTransformInPlace(CryptoContextImpl<DCRTPoly>& ctx, + Ciphertext<DCRTPoly>& ct, + int gStep, + int bStep, + const std::vector<Plaintext>& pts, + Plaintext& mask, + const std::vector<int>& indexes, + int stride, + int maskRotationStride, + int rowSize) override; + void loadContext(CryptoContextImpl<DCRTPoly>& ctx, const PublicKey<DCRTPoly>& publicKey) override; + void loadPlaintext(CryptoContextImpl<DCRTPoly>& ctx, Plaintext& pt) override; + void loadCiphertext(CryptoContextImpl<DCRTPoly>& ctx, Ciphertext<DCRTPoly>& ct) override; + std::shared_ptr<void> evalFastRotationPrecompute(CryptoContextImpl<DCRTPoly>& ctx, const Ciphertext<DCRTPoly>& ct) override; + + std::any cloneCiphertextBackend(CryptoContextImpl<DCRTPoly>& ctx, const CiphertextImpl<DCRTPoly>& src) override; + size_t ciphertextLevel(CryptoContextImpl<DCRTPoly>& ctx, const CiphertextImpl<DCRTPoly>& ct) override; + size_t ciphertextNoiseScaleDeg(CryptoContextImpl<DCRTPoly>& ctx, const CiphertextImpl<DCRTPoly>& ct) override; + void setCiphertextSlots(CryptoContextImpl<DCRTPoly>& ctx, CiphertextImpl<DCRTPoly>& ct, size_t slots) override; + void setCiphertextLevel(CryptoContextImpl<DCRTPoly>& ctx, CiphertextImpl<DCRTPoly>& ct, size_t level) override; + + bool isContextLoaded() const override; + void synchronize() const override; + void teardown() override; + void setDevices(const std::vector<int>& devices) override; + std::vector<int> devices() const override; + + // ---- CUDA-owned per-context state ---- + private: + /// @brief CUDA CKKS context; null == not loaded. Owned by this engine (one engine per context). + std::unique_ptr<FIDESlib::CKKS::Context> context_; + /// @brief Devices this context is loaded on (default: device 0). + std::vector<int> devices_ = { 0 }; + + public: + ~CudaEngine() override; +}; + +} // namespace fideslib + +#endif diff --git a/examples/advanced/src/advanced.cpp b/examples/advanced/src/advanced.cpp index fa1ce891..e7f6ff20 100644 --- a/examples/advanced/src/advanced.cpp +++ b/examples/advanced/src/advanced.cpp @@ -76,7 +76,7 @@ void AutomaticRescaleDemo(ScalingTechnique scalTech) { parameters.SetScalingModSize(50); parameters.SetScalingTechnique(scalTech); parameters.SetBatchSize(batchSize); - parameters.SetDevices(std::vector(devices)); + parameters.SetBackend(Backend::CUDA); CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters); @@ -125,7 +125,7 @@ void ManualRescaleDemo(ScalingTechnique scalTech) { parameters.SetScalingModSize(50); parameters.SetBatchSize(batchSize); parameters.SetScalingTechnique(scalTech); - parameters.SetDevices(std::vector(devices)); + parameters.SetBackend(Backend::CUDA); CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters); @@ -189,7 +189,7 @@ void HybridKeySwitchingDemo1() { parameters.SetBatchSize(batchSize); parameters.SetScalingTechnique(FLEXIBLEAUTO); parameters.SetNumLargeDigits(dnum); - parameters.SetDevices(std::vector(devices)); + parameters.SetBackend(Backend::CUDA); CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters); @@ -244,7 +244,7 @@ void HybridKeySwitchingDemo2() { parameters.SetBatchSize(batchSize); parameters.SetScalingTechnique(FLEXIBLEAUTO); parameters.SetNumLargeDigits(dnum); - parameters.SetDevices(std::vector(devices)); + parameters.SetBackend(Backend::CUDA); CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters); @@ -299,7 +299,7 @@ void FastRotationsDemo1() { parameters.SetBatchSize(batchSize); parameters.SetScalingTechnique(FLEXIBLEAUTO); parameters.SetNumLargeDigits(dnum); - parameters.SetDevices(std::vector(devices)); + parameters.SetBackend(Backend::CUDA); CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters); @@ -399,7 +399,7 @@ void FastRotationsDemo2() { // parameters.SetKeySwitchTechnique(BV); parameters.SetFirstModSize(60); parameters.SetDigitSize(digitSize); - parameters.SetDevices(std::vector(devices)); + parameters.SetBackend(Backend::CUDA); CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters); diff --git a/examples/bert-tiny/main/berttiny-all.cu b/examples/bert-tiny/main/berttiny-all.cu old mode 100755 new mode 100644 diff --git a/examples/bert-tiny/src/Inputs.cu b/examples/bert-tiny/src/Inputs.cu old mode 100755 new mode 100644 diff --git a/examples/bert-tiny/src/Inputs.cuh b/examples/bert-tiny/src/Inputs.cuh old mode 100755 new mode 100644 diff --git a/examples/bert-tiny/src/utils.cu b/examples/bert-tiny/src/utils.cu old mode 100755 new mode 100644 diff --git a/examples/bert-tiny/src/utils.cuh b/examples/bert-tiny/src/utils.cuh old mode 100755 new mode 100644 diff --git a/examples/bert-tiny/test/BERTTinyTests.cu b/examples/bert-tiny/test/BERTTinyTests.cu old mode 100755 new mode 100644 diff --git a/examples/bert-tiny/test/MMTests.cu b/examples/bert-tiny/test/MMTests.cu old mode 100755 new mode 100644 diff --git a/examples/bert-tiny/test/ParametrizedTest.cuh b/examples/bert-tiny/test/ParametrizedTest.cuh old mode 100755 new mode 100644 diff --git a/examples/bert-tiny/test/PolyApproxTests.cu b/examples/bert-tiny/test/PolyApproxTests.cu old mode 100755 new mode 100644 diff --git a/examples/bert-tiny/test/TTests1.cu b/examples/bert-tiny/test/TTests1.cu old mode 100755 new mode 100644 diff --git a/examples/bootstrap/src/bootstrap.cpp b/examples/bootstrap/src/bootstrap.cpp index 02806f7a..4f10da59 100644 --- a/examples/bootstrap/src/bootstrap.cpp +++ b/examples/bootstrap/src/bootstrap.cpp @@ -72,7 +72,7 @@ void SimpleBootstrapExample() { parameters.SetScalingTechnique(rescaleTech); parameters.SetFirstModSize(firstMod); parameters.SetKeySwitchTechnique(HYBRID); - parameters.SetDevices(std::vector(devices)); + parameters.SetBackend(Backend::CUDA); std::vector<uint32_t> levelBudget = { 3, 3 }; uint32_t levelsAvailableAfterBootstrap = 10; @@ -97,7 +97,7 @@ void SimpleBootstrapExample() { cryptoContext->LoadContext(keyPair.publicKey); cryptoContext->EvalBootstrapSetup(levelBudget, { 0, 0 }, numSlots, 0); - cryptoContext->EvalBootstrapKeyGen(keyPair, numSlots); + cryptoContext->EvalBootstrapKeyGen(keyPair.secretKey, numSlots); std::vector<double> x = { 0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 4.0, 5.0 }; size_t encodedLength = x.size(); @@ -130,7 +130,7 @@ void BootstrapExample(uint32_t numSlots) { parameters.SetRingDim(1 << 12); parameters.SetNumLargeDigits(3); parameters.SetKeySwitchTechnique(HYBRID); - parameters.SetDevices(std::vector(devices)); + parameters.SetBackend(Backend::CUDA); #if NATIVEINT == 128 && !defined(__EMSCRIPTEN__) ScalingTechnique rescaleTech = FIXEDAUTO; @@ -170,7 +170,7 @@ void BootstrapExample(uint32_t numSlots) { cryptoContext->LoadContext(keyPair.publicKey); cryptoContext->EvalBootstrapSetup(levelBudget, bsgsDim, numSlots, 0); - cryptoContext->EvalBootstrapKeyGen(keyPair, numSlots); + cryptoContext->EvalBootstrapKeyGen(keyPair.secretKey, numSlots); std::vector<double> x; std::random_device rd; @@ -212,7 +212,7 @@ void BootstrapExampleSSE(uint32_t numSlots) { parameters.SetRingDim(1 << 12); parameters.SetNumLargeDigits(3); parameters.SetKeySwitchTechnique(HYBRID); - parameters.SetDevices(std::vector(devices)); + parameters.SetBackend(Backend::CUDA); #if NATIVEINT == 128 && !defined(__EMSCRIPTEN__) ScalingTechnique rescaleTech = FIXEDAUTO; @@ -252,7 +252,7 @@ void BootstrapExampleSSE(uint32_t numSlots) { cryptoContext->LoadContext(keyPair.publicKey); cryptoContext->EvalBootstrapSetup(levelBudget, bsgsDim, numSlots, 0); - cryptoContext->EvalBootstrapKeyGen(keyPair, numSlots); + cryptoContext->EvalBootstrapKeyGen(keyPair.secretKey, numSlots); std::vector<double> x; std::random_device rd; diff --git a/examples/hpca/solutions/02_polynomials.cpp b/examples/hpca/solutions/02_polynomials.cpp index 3cca55de..4d65f4d0 100644 --- a/examples/hpca/solutions/02_polynomials.cpp +++ b/examples/hpca/solutions/02_polynomials.cpp @@ -79,7 +79,7 @@ int main() { parameters.SetKeySwitchTechnique(HYBRID); parameters.SetNumLargeDigits(dnum); parameters.SetBatchSize(batchSize); - parameters.SetDevices({ 0 }); + parameters.SetBackend(Backend::CUDA); parameters.SetPlaintextAutoload(false); parameters.SetCiphertextAutoload(true); diff --git a/examples/hpca/solutions/03_simd.cpp b/examples/hpca/solutions/03_simd.cpp index a594b7a8..36bd722f 100644 --- a/examples/hpca/solutions/03_simd.cpp +++ b/examples/hpca/solutions/03_simd.cpp @@ -195,7 +195,7 @@ int main() { parameters.SetKeySwitchTechnique(HYBRID); parameters.SetNumLargeDigits(dnum); parameters.SetBatchSize(batchSize); - parameters.SetDevices({ 0 }); + parameters.SetBackend(Backend::CUDA); parameters.SetPlaintextAutoload(false); parameters.SetCiphertextAutoload(true); diff --git a/examples/hpca/solutions/04_optimizations.cpp b/examples/hpca/solutions/04_optimizations.cpp index e1b0cff7..e1c168a5 100644 --- a/examples/hpca/solutions/04_optimizations.cpp +++ b/examples/hpca/solutions/04_optimizations.cpp @@ -137,7 +137,7 @@ void sparse_bootstrap() { params.SetKeySwitchTechnique(HYBRID); params.SetNumLargeDigits(dnum); params.SetBatchSize(batchSize); - params.SetDevices({ 0 }); + params.SetBackend(Backend::CUDA); params.SetCiphertextAutoload(true); // ======== @@ -206,7 +206,7 @@ void uniform_bootstrap() { params.SetKeySwitchTechnique(HYBRID); params.SetNumLargeDigits(dnum); params.SetBatchSize(batchSize); - params.SetDevices({ 0 }); + params.SetBackend(Backend::CUDA); params.SetCiphertextAutoload(true); // ======== @@ -276,7 +276,7 @@ int main() { parameters.SetKeySwitchTechnique(HYBRID); parameters.SetNumLargeDigits(dnum); parameters.SetBatchSize(batchSize); - parameters.SetDevices({ 0 }); + parameters.SetBackend(Backend::CUDA); parameters.SetPlaintextAutoload(false); parameters.SetCiphertextAutoload(true); diff --git a/examples/hpca/solutions/05_gaussian.cpp b/examples/hpca/solutions/05_gaussian.cpp index 41047bd3..6133279e 100644 --- a/examples/hpca/solutions/05_gaussian.cpp +++ b/examples/hpca/solutions/05_gaussian.cpp @@ -392,7 +392,7 @@ int main(int argc, char* argv[]) { parameters.SetKeySwitchTechnique(HYBRID); parameters.SetNumLargeDigits(dnum); parameters.SetBatchSize(batchSize); - parameters.SetDevices({ 0 }); + parameters.SetBackend(Backend::CUDA); parameters.SetPlaintextAutoload(false); parameters.SetCiphertextAutoload(true); diff --git a/examples/hpca/src/00_basic_workflow.cpp b/examples/hpca/src/00_basic_workflow.cpp index e54a1315..8e3bcde7 100644 --- a/examples/hpca/src/00_basic_workflow.cpp +++ b/examples/hpca/src/00_basic_workflow.cpp @@ -36,7 +36,7 @@ int main() { parameters.SetNumLargeDigits(dnum); parameters.SetBatchSize(batchSize); // GPU Settings. Devices and autoload configuration. - parameters.SetDevices({ 0 }); + parameters.SetBackend(Backend::CUDA); parameters.SetPlaintextAutoload(false); parameters.SetCiphertextAutoload(true); diff --git a/examples/hpca/src/01_bootstrapping.cpp b/examples/hpca/src/01_bootstrapping.cpp index 42e9c4c4..9ed7695c 100644 --- a/examples/hpca/src/01_bootstrapping.cpp +++ b/examples/hpca/src/01_bootstrapping.cpp @@ -36,7 +36,7 @@ int main() { parameters.SetNumLargeDigits(dnum); parameters.SetBatchSize(batchSize); // GPU Settings. Devices and autoload configuration. - parameters.SetDevices({ 0 }); + parameters.SetBackend(Backend::CUDA); parameters.SetPlaintextAutoload(false); parameters.SetCiphertextAutoload(true); diff --git a/examples/hpca/src/02_polynomials.cpp b/examples/hpca/src/02_polynomials.cpp index a9922ccd..5f1aa662 100644 --- a/examples/hpca/src/02_polynomials.cpp +++ b/examples/hpca/src/02_polynomials.cpp @@ -62,7 +62,7 @@ int main() { parameters.SetKeySwitchTechnique(HYBRID); parameters.SetNumLargeDigits(dnum); parameters.SetBatchSize(batchSize); - parameters.SetDevices({ 0 }); + parameters.SetBackend(Backend::CUDA); parameters.SetPlaintextAutoload(false); parameters.SetCiphertextAutoload(true); diff --git a/examples/hpca/src/03_simd.cpp b/examples/hpca/src/03_simd.cpp index 3d04d586..9a312c51 100644 --- a/examples/hpca/src/03_simd.cpp +++ b/examples/hpca/src/03_simd.cpp @@ -172,7 +172,7 @@ int main() { parameters.SetKeySwitchTechnique(HYBRID); parameters.SetNumLargeDigits(dnum); parameters.SetBatchSize(batchSize); - parameters.SetDevices({ 0 }); + parameters.SetBackend(Backend::CUDA); parameters.SetPlaintextAutoload(false); parameters.SetCiphertextAutoload(true); diff --git a/examples/hpca/src/04_optimizations.cpp b/examples/hpca/src/04_optimizations.cpp index 64bc1d2e..7477e8fd 100644 --- a/examples/hpca/src/04_optimizations.cpp +++ b/examples/hpca/src/04_optimizations.cpp @@ -126,7 +126,7 @@ void sparse_bootstrap() { std::vector<uint32_t> bsgs = { 16, 16 }; CCParams<CryptoContextCKKSRNS> params; - params.SetDevices({ 0 }); + params.SetBackend(Backend::CUDA); params.SetSecurityLevel(SecurityLevel::HEStd_NotSet); params.SetRingDim(ring_dim); params.SetMultiplicativeDepth(multDepth); @@ -196,7 +196,7 @@ void uniform_bootstrap() { std::vector<uint32_t> bsgs = { 16, 16 }; CCParams<CryptoContextCKKSRNS> params; - params.SetDevices({ 0 }); + params.SetBackend(Backend::CUDA); params.SetSecurityLevel(SecurityLevel::HEStd_NotSet); params.SetRingDim(ring_dim); params.SetMultiplicativeDepth(multDepth); @@ -275,7 +275,7 @@ int main() { parameters.SetKeySwitchTechnique(HYBRID); parameters.SetNumLargeDigits(dnum); parameters.SetBatchSize(batchSize); - parameters.SetDevices({ 0 }); + parameters.SetBackend(Backend::CUDA); parameters.SetPlaintextAutoload(false); parameters.SetCiphertextAutoload(true); diff --git a/examples/hpca/src/05_gaussian.cpp b/examples/hpca/src/05_gaussian.cpp index 94a8bb4d..d1e666c7 100644 --- a/examples/hpca/src/05_gaussian.cpp +++ b/examples/hpca/src/05_gaussian.cpp @@ -371,7 +371,7 @@ int main(int argc, char* argv[]) { parameters.SetKeySwitchTechnique(HYBRID); parameters.SetNumLargeDigits(dnum); parameters.SetBatchSize(batchSize); - parameters.SetDevices({ 0 }); + parameters.SetBackend(Backend::CUDA); parameters.SetPlaintextAutoload(false); parameters.SetCiphertextAutoload(true); diff --git a/examples/logreg/CMakeLists.txt b/examples/logreg/CMakeLists.txt index ad118ac8..5b068a12 100644 --- a/examples/logreg/CMakeLists.txt +++ b/examples/logreg/CMakeLists.txt @@ -35,5 +35,8 @@ add_executable(logreg) target_sources(logreg PRIVATE ${CXX_SOURCE_FILES}) -target_link_libraries(logreg PRIVATE fideslib::fideslib - PRIVATE CUDA::cudart) \ No newline at end of file +find_package(CUDAToolkit QUIET) +target_link_libraries(logreg PRIVATE fideslib::fideslib) +if(CUDAToolkit_FOUND) + target_link_libraries(logreg PRIVATE CUDA::cudart) +endif() \ No newline at end of file diff --git a/examples/logreg/src/fhe.cpp b/examples/logreg/src/fhe.cpp index d5a69d89..5e4f6be2 100644 --- a/examples/logreg/src/fhe.cpp +++ b/examples/logreg/src/fhe.cpp @@ -45,10 +45,13 @@ uint32_t create_context(bool inference) { params.SetKeySwitchTechnique(fideslib::HYBRID); params.SetSecretKeyDist(sparse_encaps ? fideslib::SPARSE_TERNARY : fideslib::UNIFORM_TERNARY); params.SetNumLargeDigits(digits); - params.SetDevices(std::vector<int>(devices)); + params.SetBackend(fideslib::Backend::CUDA); params.SetMultiplicativeDepth(depth); cc = GenCryptoContext(params); + if (!devices.empty()) { + cc->SetCudaDevices(devices); + } cc->Enable(fideslib::FHE); cc->Enable(fideslib::PKE); cc->Enable(fideslib::LEVELEDSHE); diff --git a/examples/resnet/CMakeLists.txt b/examples/resnet/CMakeLists.txt index 1d02d8e9..6ee7a589 100644 --- a/examples/resnet/CMakeLists.txt +++ b/examples/resnet/CMakeLists.txt @@ -40,4 +40,9 @@ add_executable (resnet) target_sources(resnet PRIVATE ${CXX_SOURCE_FILES}) +# resnet uses #include <fideslib/...> style includes. Derive the include root from the +# fideslib cmake dir (e.g. <prefix>/share/fideslib/cmake -> <prefix>/include). +get_filename_component(_fideslib_prefix "${fideslib_DIR}/../../.." ABSOLUTE) +target_include_directories(resnet PRIVATE "${_fideslib_prefix}/include") + target_link_libraries (resnet PRIVATE fideslib::fideslib) \ No newline at end of file diff --git a/examples/resnet/src/controller.cpp b/examples/resnet/src/controller.cpp index 1e951413..58b08488 100644 --- a/examples/resnet/src/controller.cpp +++ b/examples/resnet/src/controller.cpp @@ -46,7 +46,7 @@ void resnet::generate_context(experiment_settings e) { std::iota(this->devices.begin(), this->devices.end(), 0); } - parameters.SetDevices(std::vector(this->devices)); + parameters.SetBackend(fideslib::Backend::CUDA); parameters.SetSecretKeyDist(e.secret_key_dist); parameters.SetPlaintextAutoload(e.autoload); @@ -69,6 +69,9 @@ void resnet::generate_context(experiment_settings e) { parameters.SetMultiplicativeDepth(this->circuit_depth); context = GenCryptoContext(parameters); + if (!this->devices.empty()) { + context->SetCudaDevices(this->devices); + } std::cout << "Context built, generating keys..." << std::endl; @@ -221,14 +224,15 @@ void resnet::deserialize_context(experiment_settings e) { const char* env_devices = std::getenv("FIDESLIB_DEVICES"); if (env_devices) { - this->context->devices.clear(); + std::vector<int> dev_override; std::string str_devices(env_devices); std::replace(str_devices.begin(), str_devices.end(), ',', ' '); std::stringstream ss(str_devices); int device_id; while (ss >> device_id) { - this->context->devices.push_back(device_id); + dev_override.push_back(device_id); } + this->context->SetCudaDevices(dev_override); } key_pair.publicKey = clientPublicKey; @@ -265,7 +269,7 @@ void resnet::deserialize_context(experiment_settings e) { this->auto_load_ciphertexts = context->auto_load_ciphertexts; this->auto_load_plaintexts = context->auto_load_plaintexts; - this->devices = std::vector(this->context->devices); + this->devices = this->context->GetCudaDevices(); this->prescaled = e.prescale; this->partial_load = e.by_layer_loading; diff --git a/examples/serial/src/serial.cpp b/examples/serial/src/serial.cpp index 57a0e0c4..e28d5168 100644 --- a/examples/serial/src/serial.cpp +++ b/examples/serial/src/serial.cpp @@ -52,7 +52,7 @@ CCParams<CryptoContextCKKSRNS> parameters; parameters.SetRingDim(1 << 12); parameters.SetNumLargeDigits(3); parameters.SetKeySwitchTechnique(HYBRID); - parameters.SetDevices(std::vector(devices)); + parameters.SetBackend(Backend::CUDA); #if NATIVEINT == 128 && !defined(__EMSCRIPTEN__) ScalingTechnique rescaleTech = FIXEDAUTO; diff --git a/examples/simple/src/simple.cpp b/examples/simple/src/simple.cpp index 752ce6e2..9155821d 100644 --- a/examples/simple/src/simple.cpp +++ b/examples/simple/src/simple.cpp @@ -47,7 +47,7 @@ int main() { parameters.SetMultiplicativeDepth(multDepth); parameters.SetScalingModSize(scaleModSize); parameters.SetBatchSize(batchSize); - parameters.SetDevices({ 0 }); + parameters.SetBackend(Backend::CUDA); parameters.SetPlaintextAutoload(false); parameters.SetCiphertextAutoload(true); diff --git a/src/CKKS/LinearTransform.cu b/src/CKKS/LinearTransform.cu index 8b4acd6d..e966c511 100644 --- a/src/CKKS/LinearTransform.cu +++ b/src/CKKS/LinearTransform.cu @@ -650,31 +650,6 @@ void FIDESlib::CKKS::SpecialConvolutionTransform(Ciphertext& ctxt, } } -std::vector<int> FIDESlib::CKKS::GetConvolutionTransformRotationIndices(int rowSize, int bStep, int stride, uint32_t gStep) { - std::vector<int> res; - // Internal block size for DotProductPtInternal - constexpr uint32_t INTERNAL_GSTEP = 8; - - // Intra-block rotations: stride * k for k in [1, INTERNAL_GSTEP] - // We need rotations up to the max block size used, which is min(gStep, INTERNAL_GSTEP) - // But to be safe and simple, we can generate up to INTERNAL_GSTEP if gStep >= INTERNAL_GSTEP - uint32_t maxIntra = std::min(gStep, INTERNAL_GSTEP); - for (uint32_t k = 1; k < maxIntra; ++k) { - res.push_back(stride * k); - } - - // Inter-block rotations: stride * INTERNAL_GSTEP * k for k in [1, blockCount - 1] - uint32_t blockCount = (gStep + INTERNAL_GSTEP - 1) / INTERNAL_GSTEP; - if (blockCount > 1) { - int baseRotation = INTERNAL_GSTEP * stride; - for (uint32_t k = 1; k < blockCount; ++k) { - res.push_back(baseRotation * k); - } - } - - return res; -} - namespace FIDESlib::CKKS { /* diff --git a/src/CKKS/LinearTransform.cuh b/src/CKKS/LinearTransform.cuh index 38cacd1f..6cbd0d99 100644 --- a/src/CKKS/LinearTransform.cuh +++ b/src/CKKS/LinearTransform.cuh @@ -21,8 +21,6 @@ std::vector<int> GetLinearTransformPlaintextRotationIndices(int rowSize, int bSt // ConvolutionTransform: Like LinearTransform but with INVERTED order (rotate then sum, forward loop) void ConvolutionTransform(Ciphertext& ctxt, int rowSize, int bStep, const std::vector<Plaintext*>& pts, int stride, const std::vector<int>& indexes, uint32_t gStep); -std::vector<int> GetConvolutionTransformRotationIndices(int rowSize, int bStep, int stride, uint32_t gStep); - // SpecialConvolutionTransform: Like ConvolutionTransform but with special masking logic // After each gStep's bStep sum: 3 rotations with additions + mask multiplication before accumulation void SpecialConvolutionTransform(Ciphertext& ctxt, diff --git a/src/PolyApprox.cu b/src/PolyApprox.cu deleted file mode 100644 index 2d6f799c..00000000 --- a/src/PolyApprox.cu +++ /dev/null @@ -1,34 +0,0 @@ -// -// Created by seyda on 5/19/25. -// - -#include "PolyApprox.cuh" - -#include "CKKS/AccumulateBroadcast.cuh" - -namespace FIDESlib::CKKS { - -std::vector<double> get_chebyshev_coefficients(const std::function<double(double)>& func, const double a, const double b, const uint32_t degree) { - if (!degree) { - OPENFHE_THROW("The degree of approximation can not be zero"); - } - - const size_t coeffTotal{ degree + 1 }; - const double bMinusA = 0.5 * (b - a); - const double bPlusA = 0.5 * (b + a); - const double PiByDeg = M_PI / static_cast<double>(coeffTotal); - std::vector<double> functionPoints(coeffTotal); - for (size_t i = 0; i < coeffTotal; ++i) - functionPoints[i] = func(std::cos(PiByDeg * (i + 0.5)) * bMinusA + bPlusA); - - const double multFactor = 2.0 / static_cast<double>(coeffTotal); - std::vector<double> coefficients(coeffTotal); - for (size_t i = 0; i < coeffTotal; ++i) { - for (size_t j = 0; j < coeffTotal; ++j) - coefficients[i] += functionPoints[j] * std::cos(PiByDeg * i * (j + 0.5)); - coefficients[i] *= multFactor; - } - return coefficients; -} - -} // namespace FIDESlib::CKKS diff --git a/src/PolyApprox.cuh b/src/PolyApprox.cuh deleted file mode 100644 index 2ba4b5ae..00000000 --- a/src/PolyApprox.cuh +++ /dev/null @@ -1,18 +0,0 @@ -// -// Created by seyda on 5/19/25. -// -#pragma once - -#include <cmath> -#include <iostream> -#include <vector> - -#include "CKKS/ApproxModEval.cuh" -#include "CKKS/Ciphertext.cuh" -#include "CKKS/Context.cuh" -#include "pke/openfhe.h" - -namespace FIDESlib::CKKS { -std::vector<double> get_chebyshev_coefficients(const std::function<double(double)>& func, double a, double b, uint32_t degree); - -} // namespace FIDESlib::CKKS \ No newline at end of file diff --git a/test/ApiParityTest.cpp b/test/ApiParityTest.cpp new file mode 100644 index 00000000..5bbb182d --- /dev/null +++ b/test/ApiParityTest.cpp @@ -0,0 +1,389 @@ +// api-vs-OpenFHE parity suite. +// +// Drives the public fideslib api on the CUDA backend and asserts the resulting ciphertext is +// bit-for-bit identical to the same computation run directly through OpenFHE (the oracle). This is +// the api-level counterpart of the raw-GPU parity tests in OpenFheInterfaceTests.cu: instead of +// hand-plumbing FIDESlib::CKKS device objects, every op goes through CryptoContextImpl, and the +// result is recovered with CryptoContextImpl::RecoverHostCiphertext (the backend readback seam). +// +// Because it compares the active backend against OpenFHE, it is only meaningful on a non-CPU +// backend: on the CPU backend the api *is* OpenFHE, so the comparison is trivial and the fixture +// skips. Set FIDESLIB_TEST_BACKEND=cuda on a CUDA build to run it against the CUDA engine. The same +// tests will exercise a future Haze backend (Haze == OpenFHE) unchanged, just by selecting it. +// +// Oracle note: when several ops are chained, the api runs them on the device and only the final +// result is synced back, so intermediate host shadows are stale. The oracle therefore replays the +// whole chain in OpenFHE from the freshly-encrypted inputs (never from a mid-chain api ciphertext). + +#include <gtest/gtest.h> +#include <openfhe.h> + +#include <any> +#include <cmath> +#include <string> +#include <vector> + +#include "fideslib.hpp" + +using namespace fideslib; + +// Exact ciphertext equality: scaling metadata + every RNS limb of both polynomials. Mirrors the +// ASSERT_EQ_CIPHERTEXT in test/ParametrizedTest.cuh, minus the per-limb debug printing. +#define ASSERT_EQ_CIPHERTEXT(ct1, ct2) \ + do { \ + ASSERT_EQ((ct1)->GetNoiseScaleDeg(), (ct2)->GetNoiseScaleDeg()); \ + ASSERT_EQ((ct1)->GetScalingFactor(), (ct2)->GetScalingFactor()); \ + ASSERT_EQ((ct1)->GetEncodingType(), (ct2)->GetEncodingType()); \ + for (size_t j = 0; j < 2; ++j) { \ + ASSERT_EQ((ct1)->GetElements().at(j).GetAllElements().size(), (ct2)->GetElements().at(j).GetAllElements().size()); \ + for (size_t i = 0; i < (ct1)->GetElements().at(j).GetAllElements().size(); ++i) { \ + ASSERT_EQ((ct1)->GetElements().at(j).GetAllElements().at(i).GetValues().GetLength(), \ + (ct2)->GetElements().at(j).GetAllElements().at(i).GetValues().GetLength()); \ + ASSERT_EQ((ct1)->GetElements().at(j).GetAllElements().at(i).GetValues(), (ct2)->GetElements().at(j).GetAllElements().at(i).GetValues()); \ + } \ + } \ + } while (0) + +static bool TestUseCuda() { + const char* b = std::getenv("FIDESLIB_TEST_BACKEND"); + return b != nullptr && std::string(b) == "cuda" && IsBackendAvailable(Backend::CUDA); +} + +// The OpenFHE context the api wraps (its CPU shadow). It holds the same keys the api generated, so +// it is the parity oracle. +static lbcrypto::CryptoContext<lbcrypto::DCRTPoly>& LbCc(CryptoContext<DCRTPoly>& cc) { + return std::any_cast<lbcrypto::CryptoContext<lbcrypto::DCRTPoly>&>(cc->host); +} + +// The lbcrypto ciphertext behind a freshly-encrypted api ciphertext (host-resident before any op). +// Only valid on encrypt outputs — a mid-chain api ciphertext's shadow is stale on the CUDA backend. +static lbcrypto::Ciphertext<lbcrypto::DCRTPoly> LbCt(Ciphertext<DCRTPoly>& ct) { + return std::any_cast<lbcrypto::Ciphertext<lbcrypto::DCRTPoly>>(ct->host); +} + +// The lbcrypto plaintext behind an api plaintext. +static lbcrypto::Plaintext LbPt(Plaintext& pt) { + return std::any_cast<lbcrypto::Plaintext>(pt->host); +} + +// Recover the lbcrypto ciphertext of an api result, syncing it back from the device first (the device +// copy stays resident, so the ciphertext can still be used on-device afterward). +static lbcrypto::Ciphertext<lbcrypto::DCRTPoly> HostCt(CryptoContext<DCRTPoly>& cc, Ciphertext<DCRTPoly>& ct) { + cc->RecoverHostCiphertext(ct); + return std::any_cast<lbcrypto::Ciphertext<lbcrypto::DCRTPoly>>(ct->host); +} + +// Decrypt two lbcrypto ciphertexts and compare their real slots within tol. For ops that are +// numerically correct but not bit-identical to OpenFHE (the raw OpenFheInterfaceTests use +// ASSERT_ERROR_OK for the same ops, for the same reason). +static void ExpectSlotsNear(CryptoContext<DCRTPoly>& cc, + const PrivateKey<DCRTPoly>& sk, + const lbcrypto::Ciphertext<lbcrypto::DCRTPoly>& got, + const lbcrypto::Ciphertext<lbcrypto::DCRTPoly>& oracle, + size_t slots, + double tol) { + auto& lbcc = LbCc(cc); + auto& skImpl = std::any_cast<const lbcrypto::PrivateKey<lbcrypto::DCRTPoly>&>(sk->pimpl); + lbcrypto::Plaintext pgot, porc; + lbcc->Decrypt(skImpl, got, &pgot); + lbcc->Decrypt(skImpl, oracle, &porc); + pgot->SetLength(slots); + porc->SetLength(slots); + auto vg = pgot->GetRealPackedValue(); + auto vo = porc->GetRealPackedValue(); + for (size_t i = 0; i < slots; ++i) + EXPECT_NEAR(vg[i], vo[i], tol) << "slot " << i; +} + +// Same parameters as the api correctness fixture (ApiTests.cpp): depth 5, ringDim 65536, 8 slots, +// FIXEDAUTO (default secret-key dist, as in ApiTests). +class ApiParityTest : public ::testing::Test { + protected: + static constexpr uint32_t kDepth = 5; + static constexpr uint32_t kRingDim = 1u << 16; + static constexpr uint32_t kSlots = 8; + + CryptoContext<DCRTPoly> cc; + KeyPair<DCRTPoly> keys; + + const std::vector<double> v1 = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 }; + const std::vector<double> v2 = { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8 }; + // Near-1 values keep repeated squaring well-conditioned (above the noise floor, below overflow). + const std::vector<double> vsq = { 0.93, 0.96, 0.99, 1.0, 1.01, 1.04, 1.07, 0.95 }; + + void SetUp() override { + if (!TestUseCuda()) + GTEST_SKIP() << "api-vs-OpenFHE parity is trivial on the CPU backend (api == OpenFHE); " + "set FIDESLIB_TEST_BACKEND=cuda on a CUDA build to run it."; + + CCParams<CryptoContextCKKSRNS> params; + params.SetMultiplicativeDepth(kDepth); + params.SetScalingModSize(50); + params.SetBatchSize(kSlots); + params.SetRingDim(kRingDim); + params.SetScalingTechnique(FIXEDAUTO); + params.SetBackend(Backend::CUDA); + cc = GenCryptoContext(params); + cc->Enable(PKE); + cc->Enable(KEYSWITCH); + cc->Enable(LEVELEDSHE); + cc->Enable(ADVANCEDSHE); + keys = cc->KeyGen(); + cc->EvalMultKeyGen(keys.secretKey); + cc->EvalRotateKeyGen(keys.secretKey, { 1, 2, 3, 4, 5, 6, 7, 8, -1, -2 }); + cc->LoadContext(keys.publicKey); + } + + // Encrypt v1 and v2 with the public key. + void EncryptInputs(Ciphertext<DCRTPoly>& a1, Ciphertext<DCRTPoly>& a2) { + auto p1 = cc->MakeCKKSPackedPlaintext(v1); + auto p2 = cc->MakeCKKSPackedPlaintext(v2); + a1 = cc->Encrypt(p1, keys.publicKey); + a2 = cc->Encrypt(p2, keys.publicKey); + } + + // Encrypt just v1 (for single-input ops). + Ciphertext<DCRTPoly> EncryptV1() { + auto p = cc->MakeCKKSPackedPlaintext(v1); + return cc->Encrypt(p, keys.publicKey); + } + + // Approximate comparison for the single-op tests (negate/rotate/rescale). + void ExpectApproxEq(const lbcrypto::Ciphertext<lbcrypto::DCRTPoly>& got, const lbcrypto::Ciphertext<lbcrypto::DCRTPoly>& oracle) { + ExpectSlotsNear(cc, keys.secretKey, got, oracle, kSlots, 1e-6); + } +}; + +TEST_F(ApiParityTest, EvalAdd) { + Ciphertext<DCRTPoly> a1, a2; + EncryptInputs(a1, a2); + auto lb1 = LbCt(a1), lb2 = LbCt(a2); + auto res = cc->EvalAdd(a1, a2); + auto got = HostCt(cc, res); + auto oracle = LbCc(cc)->EvalAdd(lb1, lb2); + ASSERT_EQ_CIPHERTEXT(oracle, got); +} + +TEST_F(ApiParityTest, EvalSub) { + Ciphertext<DCRTPoly> a1, a2; + EncryptInputs(a1, a2); + auto lb1 = LbCt(a1), lb2 = LbCt(a2); + auto res = cc->EvalSub(a1, a2); + auto got = HostCt(cc, res); + auto oracle = LbCc(cc)->EvalSub(lb1, lb2); + ASSERT_EQ_CIPHERTEXT(oracle, got); +} + +TEST_F(ApiParityTest, EvalMult) { + Ciphertext<DCRTPoly> a1, a2; + EncryptInputs(a1, a2); + auto lb1 = LbCt(a1), lb2 = LbCt(a2); + auto res = cc->EvalMult(a1, a2); + auto got = HostCt(cc, res); + auto oracle = LbCc(cc)->EvalMult(lb1, lb2); + ASSERT_EQ_CIPHERTEXT(oracle, got); +} + +TEST_F(ApiParityTest, EvalSquare) { + auto a1 = EncryptV1(); + auto lb1 = LbCt(a1); + auto res = cc->EvalSquare(a1); + auto got = HostCt(cc, res); + auto oracle = LbCc(cc)->EvalSquare(lb1); + ASSERT_EQ_CIPHERTEXT(oracle, got); +} + +TEST_F(ApiParityTest, EvalNegate) { + auto a1 = EncryptV1(); + auto lb1 = LbCt(a1); + auto res = cc->EvalNegate(a1); + auto got = HostCt(cc, res); + auto oracle = LbCc(cc)->EvalNegate(lb1); + // Approximate, not bit-identical: the CUDA backend implements negate as multScalar(-1.0), which + // bumps NoiseScaleDeg (1->2) relative to OpenFHE's degree-preserving sign flip. The decrypted + // values match; the scale-degree bookkeeping differs (a known GPU EvalNegate fidelity gap). + ExpectApproxEq(got, oracle); +} + +TEST_F(ApiParityTest, EvalRotate) { + auto a1 = EncryptV1(); + auto lb1 = LbCt(a1); + auto res = cc->EvalRotate(a1, 1); + auto got = HostCt(cc, res); + auto oracle = LbCc(cc)->EvalRotate(lb1, 1); + // Approximate, not bit-identical: GPU rotation (hoisted automorphism + key-switch) is numerically + // correct but uses a different key-switch representation than OpenFHE. The raw OpenFheInterfaceTests + // Rotate test uses ASSERT_ERROR_OK for exactly this reason (its ASSERT_EQ_CIPHERTEXT is commented out). + ExpectApproxEq(got, oracle); +} + +TEST_F(ApiParityTest, EvalAddPlaintext) { + Ciphertext<DCRTPoly> a1, a2; + EncryptInputs(a1, a2); + auto lb1 = LbCt(a1); + auto pt = cc->MakeCKKSPackedPlaintext(v2); + auto lbpt = LbPt(pt); + auto res = cc->EvalAdd(a1, pt); + auto got = HostCt(cc, res); + auto oracle = LbCc(cc)->EvalAdd(lb1, lbpt); + ASSERT_EQ_CIPHERTEXT(oracle, got); +} + +TEST_F(ApiParityTest, EvalMultPlaintext) { + Ciphertext<DCRTPoly> a1, a2; + EncryptInputs(a1, a2); + auto lb1 = LbCt(a1); + auto pt = cc->MakeCKKSPackedPlaintext(v2); + auto lbpt = LbPt(pt); + auto res = cc->EvalMult(a1, pt); + auto got = HostCt(cc, res); + auto oracle = LbCc(cc)->EvalMult(lb1, lbpt); + ASSERT_EQ_CIPHERTEXT(oracle, got); +} + +TEST_F(ApiParityTest, EvalAddScalar) { + Ciphertext<DCRTPoly> a1, a2; + EncryptInputs(a1, a2); + auto lb1 = LbCt(a1); + auto resP = cc->EvalAdd(a1, 2.0); + auto gotP = HostCt(cc, resP); + auto oracleP = LbCc(cc)->EvalAdd(lb1, 2.0); + ASSERT_EQ_CIPHERTEXT(oracleP, gotP); + auto resN = cc->EvalAdd(a1, -2.0); + auto gotN = HostCt(cc, resN); + auto oracleN = LbCc(cc)->EvalAdd(lb1, -2.0); + ASSERT_EQ_CIPHERTEXT(oracleN, gotN); +} + +TEST_F(ApiParityTest, EvalMultScalar) { + Ciphertext<DCRTPoly> a1, a2; + EncryptInputs(a1, a2); + auto lb1 = LbCt(a1); + // 2^-7 matches the raw MultScalar test (a clean power-of-two scalar). + const double s = std::pow(2.0, -7.0); + auto res = cc->EvalMult(a1, s); + auto got = HostCt(cc, res); + auto oracle = LbCc(cc)->EvalMult(lb1, s); + ASSERT_EQ_CIPHERTEXT(oracle, got); +} + +TEST_F(ApiParityTest, RescaleAfterMult) { + Ciphertext<DCRTPoly> a1, a2; + EncryptInputs(a1, a2); + auto lb1 = LbCt(a1), lb2 = LbCt(a2); + auto prod = cc->EvalMult(a1, a2); + auto res = cc->Rescale(prod); + auto got = HostCt(cc, res); + // Oracle replays the whole chain in OpenFHE (the mid-chain product's host shadow is stale). + auto oracle = LbCc(cc)->Rescale(LbCc(cc)->EvalMult(lb1, lb2)); + // Approximate, not bit-identical: GPU and OpenFHE track NoiseScaleDeg differently across an + // explicit Rescale under FIXEDAUTO (deferred-rescale accounting). The decrypted values match. + ExpectApproxEq(got, oracle); +} + +// ---- AllLevels depth tests: exercise an op at each level down the modulus chain ---- +// These walk the ciphertext down the chain via mult+rescale, which crosses the api-Rescale +// NoiseScaleDeg divergence, so they compare decrypted values (approximate). Bit-identity at each +// individual level remains covered by the retained raw OpenFheInterfaceTests *AllLevels tests. + +TEST_F(ApiParityTest, EvalMultAllLevels) { + Ciphertext<DCRTPoly> a, b; + EncryptInputs(a, b); + auto lbA = LbCt(a); + auto lbB = LbCt(b); + for (uint32_t lvl = 0; lvl + 1 < kDepth; ++lvl) { + SCOPED_TRACE("level " + std::to_string(lvl)); + a = cc->EvalMult(a, b); + cc->RescaleInPlace(a); + lbA = LbCc(cc)->EvalMult(lbA, lbB); + LbCc(cc)->RescaleInPlace(lbA); + auto got = HostCt(cc, a); + ExpectSlotsNear(cc, keys.secretKey, got, lbA, kSlots, 1e-5); + } +} + +TEST_F(ApiParityTest, EvalSquareAllLevels) { + auto p = cc->MakeCKKSPackedPlaintext(vsq); + auto a = cc->Encrypt(p, keys.publicKey); + auto lbA = LbCt(a); + for (uint32_t lvl = 0; lvl + 1 < kDepth; ++lvl) { + SCOPED_TRACE("level " + std::to_string(lvl)); + a = cc->EvalSquare(a); + cc->RescaleInPlace(a); + lbA = LbCc(cc)->EvalSquare(lbA); + LbCc(cc)->RescaleInPlace(lbA); + auto got = HostCt(cc, a); + ExpectSlotsNear(cc, keys.secretKey, got, lbA, kSlots, 1e-5); + } +} + +TEST_F(ApiParityTest, EvalRotateAllLevels) { + Ciphertext<DCRTPoly> a, b; + EncryptInputs(a, b); + auto lbA = LbCt(a); + auto lbB = LbCt(b); + for (uint32_t lvl = 0; lvl + 1 < kDepth; ++lvl) { + SCOPED_TRACE("level " + std::to_string(lvl)); + // Rotate at the current level. + auto r = cc->EvalRotate(a, 1); + auto gotR = HostCt(cc, r); + auto oracleR = LbCc(cc)->EvalRotate(lbA, 1); + ExpectSlotsNear(cc, keys.secretKey, gotR, oracleR, kSlots, 1e-5); + // Descend a level for the next iteration. + a = cc->EvalMult(a, b); + cc->RescaleInPlace(a); + lbA = LbCc(cc)->EvalMult(lbA, lbB); + LbCc(cc)->RescaleInPlace(lbA); + } +} + +// ---- Bootstrap parity (DISABLED) ---- +// EvalBootstrap on GPU vs OpenFHE. DISABLED until the GPU FIXEDAUTO bootstrap precision defect is +// fixed (the open "Bootstrap FIXEDAUTO" item; see the plan/gpu-results notes) — enabling it now would +// only re-report that known failure. Drop the DISABLED_ prefix once that fix lands. Bootstrap is +// heavily approximate, so it compares decrypted values within the bootstrap precision (~1e-2). +class ApiParityBootstrapTest : public ::testing::Test { + protected: + static constexpr uint32_t kDepth = 25; + static constexpr uint32_t kRingDim = 1u << 16; + static constexpr uint32_t kSlots = 8; + + CryptoContext<DCRTPoly> cc; + KeyPair<DCRTPoly> keys; + const std::vector<double> v1 = { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8 }; + + void SetUp() override { + if (!TestUseCuda()) + GTEST_SKIP() << "api-vs-OpenFHE parity is trivial on the CPU backend."; + CCParams<CryptoContextCKKSRNS> params; + params.SetMultiplicativeDepth(kDepth); + params.SetScalingModSize(50); + params.SetBatchSize(kSlots); + params.SetRingDim(kRingDim); + params.SetScalingTechnique(FIXEDAUTO); + params.SetSecretKeyDist(UNIFORM_TERNARY); + params.SetSecurityLevel(HEStd_NotSet); + params.SetBackend(Backend::CUDA); + cc = GenCryptoContext(params); + cc->Enable(PKE); + cc->Enable(KEYSWITCH); + cc->Enable(LEVELEDSHE); + cc->Enable(ADVANCEDSHE); + cc->Enable(FHE); + keys = cc->KeyGen(); + cc->EvalMultKeyGen(keys.secretKey); + cc->EvalBootstrapSetup({ 1, 1 }, { 0, 0 }, kSlots, 11); + cc->EvalBootstrapKeyGen(keys.secretKey, kSlots); + cc->LoadContext(keys.publicKey); + } +}; + +TEST_F(ApiParityBootstrapTest, DISABLED_EvalBootstrap) { + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto lbCt = LbCt(ct); + auto refreshed = cc->EvalBootstrap(ct); + auto got = HostCt(cc, refreshed); + auto oracle = LbCc(cc)->EvalBootstrap(lbCt); + ExpectSlotsNear(cc, keys.secretKey, got, oracle, kSlots, 1e-2); +} diff --git a/test/ApiTests.cpp b/test/ApiTests.cpp new file mode 100644 index 00000000..a2a0565f --- /dev/null +++ b/test/ApiTests.cpp @@ -0,0 +1,1238 @@ +// API test suite for fideslib — drives the public CryptoContextImpl api and verifies +// results by decryption against expected values, so it is meaningful on either backend. +// Default backend is CPU; set FIDESLIB_TEST_BACKEND=cuda on a CUDA build to exercise the +// CUDA engine end-to-end (see TestUseCuda below). +// CPU-only build: cmake -B build-cpu -DFIDESLIB_ENABLE_CUDA=OFF && cmake --build build-cpu --target fideslib-cpu-test +// Run: ./build-cpu/fideslib-cpu-test [--gtest_filter=-*Bootstrap*] + +#include <algorithm> +#include <cmath> +#include <complex> +#include <cstdio> +#include <cstdlib> +#include <functional> +#include <gtest/gtest.h> +#include <sstream> +#include <vector> + +#include "Serialize.hpp" +#include "fideslib.hpp" + +using namespace fideslib; + +// The api test fixtures run against either backend: set FIDESLIB_TEST_BACKEND=cuda +// (on a CUDA build) to exercise the CUDA engine end-to-end. CPU-only builds always +// run on the CPU backend, since IsBackendAvailable(CUDA) is false when CUDA is not +// compiled in, so this is a no-op there. +static bool TestUseCuda() { + const char* b = std::getenv("FIDESLIB_TEST_BACKEND"); + return b != nullptr && std::string(b) == "cuda" && IsBackendAvailable(Backend::CUDA); +} + +// Precision tolerance for standard CKKS operations (encrypt, add, mult, rotate). +// With scalingModSize=50, theoretical single-op precision is ~2^{-50} ~ 1e-15. +// Accumulated error through depth-5 circuits and encoding roundtrip gives ~1e-6. +static constexpr double CKKS_PRECISION = 1e-6; + +// Bootstrapping introduces polynomial approximation error for modular reduction. +// OpenFHE's iterative bootstrap with levelBudget={1,1} achieves ~10-14 bits of +// precision; 1e-2 (~6.6 bits) provides margin for the single-iteration case. +static constexpr double CKKS_BOOTSTRAP_PRECISION = 1e-2; + +// Decrypt ct into *pt and check real slots against expected within tol. +static void CheckPrecision(CryptoContext<DCRTPoly>& cc, Ciphertext<DCRTPoly>& ct, const PrivateKey<DCRTPoly>& sk, const std::vector<double>& expected, double tol) { + Plaintext pt; + cc->Decrypt(ct, sk, &pt); + pt->SetLength(expected.size()); + auto vals = pt->GetRealPackedValue(); + for (size_t i = 0; i < expected.size(); ++i) + EXPECT_NEAR(vals[i], expected[i], tol) << "slot " << i; +} + +// --------------------------------------------------------------------------- +// Standard fixture: depth=5, ringDim=65536, slots=8, FIXEDAUTO, UNIFORM_TERNARY +// --------------------------------------------------------------------------- + +class CKKSTest : public ::testing::Test { + protected: + static constexpr uint32_t kDepth = 5; + static constexpr uint32_t kRingDim = 1u << 16; // 65536 + static constexpr uint32_t kSlots = 8; + + CryptoContext<DCRTPoly> cc; + KeyPair<DCRTPoly> keys; + + // Test vectors (8 slots). + const std::vector<double> v1 = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 }; + const std::vector<double> v2 = { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8 }; + + void SetUp() override { + CCParams<CryptoContextCKKSRNS> params; + params.SetMultiplicativeDepth(kDepth); + params.SetScalingModSize(50); + params.SetBatchSize(kSlots); + params.SetRingDim(kRingDim); + params.SetScalingTechnique(FIXEDAUTO); + if (TestUseCuda()) + params.SetBackend(Backend::CUDA); + cc = GenCryptoContext(params); + cc->Enable(PKE); + cc->Enable(KEYSWITCH); + cc->Enable(LEVELEDSHE); + cc->Enable(ADVANCEDSHE); + keys = cc->KeyGen(); + cc->EvalMultKeyGen(keys.secretKey); + // Generate every rotation index the tests use up front. In GPU mode the keys are + // uploaded by LoadContext, so they must all exist before it; the convolution and + // accumulate ops need the full 1..8 range (incl. 3,5,6,7), not just powers of two. + cc->EvalRotateKeyGen(keys.secretKey, { 1, 2, 3, 4, 5, 6, 7, 8, -1, -2 }); + if (TestUseCuda()) + cc->LoadContext(keys.publicKey); + } +}; + +// ---- 1. Context ---- + +TEST_F(CKKSTest, ContextSetup) { + EXPECT_EQ(cc->GetRingDimension(), kRingDim); + EXPECT_EQ(cc->GetCyclotomicOrder(), 2 * kRingDim); +} + +// ---- 2. Encoding ---- + +TEST_F(CKKSTest, EncodingReal) { + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto out = pt->GetRealPackedValue(); + for (size_t i = 0; i < v1.size(); ++i) + EXPECT_NEAR(out[i], v1[i], CKKS_PRECISION) << "slot " << i; +} + +TEST_F(CKKSTest, EncodingComplex) { + std::vector<std::complex<double>> cv(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + cv[i] = { v1[i], 0.0 }; + auto pt = cc->MakeCKKSPackedPlaintext(cv); + auto out = pt->GetCKKSPackedValue(); + for (size_t i = 0; i < v1.size(); ++i) + EXPECT_NEAR(out[i].real(), v1[i], CKKS_PRECISION) << "slot " << i; +} + +// ---- 3. Encrypt / Decrypt ---- + +TEST_F(CKKSTest, EncryptDecryptPublicKey) { + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + CheckPrecision(cc, ct, keys.secretKey, v1, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EncryptDecryptPrivateKey) { + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.secretKey); + CheckPrecision(cc, ct, keys.secretKey, v1, CKKS_PRECISION); +} + +// ---- 4. EvalAdd ---- + +TEST_F(CKKSTest, EvalAddCtCt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] + v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + auto res = cc->EvalAdd(ct1, ct2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalAddCtPt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] + v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto res = cc->EvalAdd(ct1, pt2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalAddCtScalar) { + double scalar = 10.0; + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] + scalar; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto res = cc->EvalAdd(ct, scalar); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalAddInPlaceCtCt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] + v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + cc->EvalAddInPlace(ct1, ct2); + CheckPrecision(cc, ct1, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalAddInPlaceCtPt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] + v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + cc->EvalAddInPlace(ct1, pt2); + CheckPrecision(cc, ct1, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalAddInPlaceCtScalar) { + double scalar = 5.0; + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] + scalar; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + cc->EvalAddInPlace(ct, scalar); + CheckPrecision(cc, ct, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalAddMany) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] + v2[i] + v1[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + auto ct3 = cc->Encrypt(pt1, keys.publicKey); + std::vector<Ciphertext<DCRTPoly>> cts = { ct1, ct2, ct3 }; + auto res = cc->EvalAddMany(cts); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +// ---- 4. EvalSub ---- + +TEST_F(CKKSTest, EvalSubCtCt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] - v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + auto res = cc->EvalSub(ct1, ct2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalSubCtPt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] - v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto res = cc->EvalSub(ct1, pt2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalSubCtScalar) { + double scalar = 0.5; + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] - scalar; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto res = cc->EvalSub(ct, scalar); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalSubInPlaceCtCt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] - v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + cc->EvalSubInPlace(ct1, ct2); + CheckPrecision(cc, ct1, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalSubInPlaceCtScalar) { + double scalar = 3.0; + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] - scalar; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + cc->EvalSubInPlace(ct, scalar); + CheckPrecision(cc, ct, keys.secretKey, expected, CKKS_PRECISION); +} + +// ---- 4. EvalMult ---- + +TEST_F(CKKSTest, EvalMultCtCt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] * v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + auto res = cc->EvalMult(ct1, ct2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalMultCtPt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] * v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto res = cc->EvalMult(ct1, pt2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalMultCtScalar) { + double scalar = 2.0; + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] * scalar; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto res = cc->EvalMult(ct, scalar); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalMultInPlaceCtPt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] * v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + cc->EvalMultInPlace(ct1, pt2); + CheckPrecision(cc, ct1, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalMultInPlaceCtScalar) { + double scalar = 3.0; + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] * scalar; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + cc->EvalMultInPlace(ct, scalar); + CheckPrecision(cc, ct, keys.secretKey, expected, CKKS_PRECISION); +} + +// ---- 4. EvalNegate / EvalSquare ---- + +TEST_F(CKKSTest, EvalNegate) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = -v1[i]; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto res = cc->EvalNegate(ct); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalSquare) { + std::vector<double> expected(v2.size()); + for (size_t i = 0; i < v2.size(); ++i) + expected[i] = v2[i] * v2[i]; + auto pt = cc->MakeCKKSPackedPlaintext(v2); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto res = cc->EvalSquare(ct); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalSquareInPlace) { + std::vector<double> expected(v2.size()); + for (size_t i = 0; i < v2.size(); ++i) + expected[i] = v2[i] * v2[i]; + auto pt = cc->MakeCKKSPackedPlaintext(v2); + auto ct = cc->Encrypt(pt, keys.publicKey); + cc->EvalSquareInPlace(ct); + CheckPrecision(cc, ct, keys.secretKey, expected, CKKS_PRECISION); +} + +// ---- 5. Rotation ---- + +TEST_F(CKKSTest, EvalRotate) { + // Rotate left by 1: slot i gets v1[(i+1) % slots] (circular). + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[(i + 1) % v1.size()]; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto res = cc->EvalRotate(ct, 1); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalFastRotation) { + // Same rotation as EvalRotate but via precompute + EvalFastRotation. + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[(i + 1) % v1.size()]; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto precomp = cc->EvalFastRotationPrecompute(ct); + uint32_t m = cc->GetCyclotomicOrder(); + auto res = cc->EvalFastRotation(ct, 1, m, precomp); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +// ---- 6. Level management ---- + +TEST_F(CKKSTest, RescaleInPlace) { + // Verify that the ciphertext remains decryptable and accurate after rescaling. + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto ct2 = cc->EvalMult(ct, ct); // ct-ct mult: scale^2 + cc->RescaleInPlace(ct2); + std::vector<double> expected(kSlots); + for (size_t i = 0; i < kSlots; ++i) + expected[i] = v1[i] * v1[i]; + CheckPrecision(cc, ct2, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, SetLevel) { + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + // Consume one level so SetLevel has somewhere to go. + cc->RescaleInPlace(ct); + size_t cur = ct->GetLevel(); + // SetLevel drops towers (increases level number toward max). + CryptoContextImpl<DCRTPoly>::SetLevel(ct, cur + 1); + EXPECT_EQ(ct->GetLevel(), cur + 1); +} + +// ---- 7. Advanced operations ---- + +TEST_F(CKKSTest, EvalChebyshevSeries) { + // The GPU Chebyshev domain-map fix is deliberately off this branch (lives on + // chebyshev-domain-map-fix); the device path is off by ~1 on non-[-1,1] intervals. + if (TestUseCuda()) + GTEST_SKIP() << "GPU Chebyshev domain-map fix deferred (branch chebyshev-domain-map-fix)"; + // Approximate the identity f(x)=x on [0,1] with degree-5 Chebyshev. + std::function<double(double)> f = [](double x) { return x; }; + auto coeffs = CryptoContextImpl<DCRTPoly>::GetChebyshevCoefficients(f, 0.0, 1.0, 5); + // Inputs must lie inside [0,1]. + std::vector<double> input(kSlots); + for (size_t i = 0; i < kSlots; ++i) + input[i] = static_cast<double>(i + 1) / (kSlots + 1); + auto pt = cc->MakeCKKSPackedPlaintext(input); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto res = cc->EvalChebyshevSeries(ct, coeffs, 0.0, 1.0); + // Degree-5 approximation of identity should be accurate to ~1e-4. + CheckPrecision(cc, res, keys.secretKey, input, 1e-4); +} + +TEST_F(CKKSTest, AccumulateSum) { + // AccumulateSum(ct, slots) sums all slots into each slot position. + // With v1={1..8}, sum = 36. + std::vector<double> expected(kSlots, 36.0); + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto res = cc->AccumulateSum(ct, static_cast<int>(kSlots)); + // Only slot 0 is guaranteed; the rest depend on rotation key availability. + Plaintext out; + cc->Decrypt(res, keys.secretKey, &out); + out->SetLength(1); + auto vals = out->GetRealPackedValue(); + EXPECT_NEAR(vals[0], 36.0, CKKS_PRECISION); +} + +// ---- 10. GetConvolutionTransformRotationIndices ---- + +TEST_F(CKKSTest, ConvRotIndices_gStep0) { + auto idx = CryptoContextImpl<DCRTPoly>::GetConvolutionTransformRotationIndices(16, 4, 1, 0); + EXPECT_TRUE(idx.empty()); +} + +TEST_F(CKKSTest, ConvRotIndices_gStep1) { + // gStep=1: maxIntra = min(1,8) = 1, loop k in [1,1) → empty intra. + // blockCount = 1 → no inter. Result: empty. + auto idx = CryptoContextImpl<DCRTPoly>::GetConvolutionTransformRotationIndices(16, 4, 1, 1); + EXPECT_TRUE(idx.empty()); +} + +TEST_F(CKKSTest, ConvRotIndices_gStep8) { + // gStep=8, stride=1: maxIntra=8, intra = {1,2,3,4,5,6,7}. + // blockCount=1 → no inter entries. + auto idx = CryptoContextImpl<DCRTPoly>::GetConvolutionTransformRotationIndices(16, 4, 1, 8); + ASSERT_EQ(idx.size(), 7u); + for (int k = 1; k <= 7; ++k) + EXPECT_EQ(idx[static_cast<size_t>(k - 1)], k); +} + +TEST_F(CKKSTest, ConvRotIndices_gStep16) { + // gStep=16, stride=2, internal_gstep=8: + // maxIntra=8, intra = {2,4,6,8,10,12,14} (k*stride for k=1..7) + // blockCount=2, inter = {16} (baseRotation=8*2=16, k=1) + auto idx = CryptoContextImpl<DCRTPoly>::GetConvolutionTransformRotationIndices(32, 4, 2, 16); + // 7 intra + 1 inter = 8 entries + ASSERT_EQ(idx.size(), 8u); + for (int k = 1; k <= 7; ++k) + EXPECT_EQ(idx[static_cast<size_t>(k - 1)], k * 2); + EXPECT_EQ(idx[7], 16); +} + +// ---- 11. In-place variants ---- + +TEST_F(CKKSTest, EvalNegateInPlace) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = -v1[i]; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + cc->EvalNegateInPlace(ct); + CheckPrecision(cc, ct, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalRotateInPlace) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[(i + 1) % v1.size()]; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + cc->EvalRotateInPlace(ct, 1); + CheckPrecision(cc, ct, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, Rescale) { + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto ct2 = cc->EvalMult(ct, ct); + auto ct3 = cc->Rescale(ct2); + std::vector<double> expected(kSlots); + for (size_t i = 0; i < kSlots; ++i) + expected[i] = v1[i] * v1[i]; + CheckPrecision(cc, ct3, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalChebyshevSeriesInPlace) { + // GPU Chebyshev domain-map fix deferred to branch chebyshev-domain-map-fix (off by ~1 on + // non-[-1,1] intervals); CPU (OpenFHE) is correct, so this still runs there. + if (TestUseCuda()) + GTEST_SKIP() << "GPU Chebyshev domain-map fix deferred (branch chebyshev-domain-map-fix)"; + std::function<double(double)> f = [](double x) { return x; }; + auto coeffs = CryptoContextImpl<DCRTPoly>::GetChebyshevCoefficients(f, 0.0, 1.0, 5); + std::vector<double> input(kSlots); + for (size_t i = 0; i < kSlots; ++i) + input[i] = static_cast<double>(i + 1) / (kSlots + 1); + auto pt = cc->MakeCKKSPackedPlaintext(input); + auto ct = cc->Encrypt(pt, keys.publicKey); + cc->EvalChebyshevSeriesInPlace(ct, coeffs, 0.0, 1.0); + CheckPrecision(cc, ct, keys.secretKey, input, 1e-4); +} + +TEST_F(CKKSTest, AccumulateSumInPlace) { + std::vector<double> expected(kSlots, 36.0); + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + cc->AccumulateSumInPlace(ct, static_cast<int>(kSlots)); + Plaintext out; + cc->Decrypt(ct, keys.secretKey, &out); + out->SetLength(1); + auto vals = out->GetRealPackedValue(); + EXPECT_NEAR(vals[0], 36.0, CKKS_PRECISION); +} + +// ---- 11b. ConvolutionTransform ---- + +TEST_F(CKKSTest, ConvolutionTransformIdentity) { + if (!TestUseCuda()) + GTEST_SKIP() << "CPU convolution is implemented in PR3 (backend-abstraction-cpu-impl)."; + // Identity transform: bStep=1, gStep=1, single plaintext of ones. + // With indexes={0} (no baby-step rotation), the result should be + // ct * pt (scaled by 1), rotated by stride*(1-0)=stride, then rescaled. + // Use a single "1.0" weight and gStep=1 so the output = rot(ct*1, stride). + std::vector<double> ones(kSlots, 1.0); + auto ptWeight = cc->MakeCKKSPackedPlaintext(ones); + std::vector<Plaintext> weights = { ptWeight }; + std::vector<int> indexes = { 0 }; + + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + + // gStep=1, bStep=1, stride=1 => rotation by stride*(1-0)=1, then rescale. + cc->ConvolutionTransformInPlace(ct, 1, 1, weights, indexes, 1); + + // Expected: v1 rotated left by 1, then ct*1.0 = v1 (precision loss from mult+rescale). + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[(i + 1) % v1.size()]; + CheckPrecision(cc, ct, keys.secretKey, expected, 1e-4); +} + +// ---- 12. SPARSE_ENCAPSULATED ---- + +TEST_F(CKKSTest, SparseEncapsulatedContext) { + CCParams<CryptoContextCKKSRNS> params; + params.SetMultiplicativeDepth(5); + params.SetScalingModSize(50); + params.SetBatchSize(kSlots); + params.SetRingDim(kRingDim); + params.SetScalingTechnique(FIXEDAUTO); + params.SetSecretKeyDist(SPARSE_ENCAPSULATED); + EXPECT_NO_THROW({ + auto cc2 = GenCryptoContext(params); + cc2->Enable(PKE); + cc2->Enable(KEYSWITCH); + cc2->Enable(LEVELEDSHE); + auto kp = cc2->KeyGen(); + auto pt = cc2->MakeCKKSPackedPlaintext(v1); + auto ct = cc2->Encrypt(pt, kp.publicKey); + Plaintext result; + cc2->Decrypt(ct, kp.secretKey, &result); + }); +} + +// ---- 13. Serialization ---- + +TEST_F(CKKSTest, SerializeDeserializeEvalMultKey) { + // Serialize the eval mult key (generated in SetUp) to a buffer. + std::ostringstream oss; + ASSERT_TRUE(CryptoContextImpl<DCRTPoly>::SerializeEvalMultKey(oss, SerType::BINARY)); + EXPECT_GT(oss.str().size(), 0u); + + // Create a second context with the same parameters but no mult key. + // Deserializing into it verifies the binary format is self-consistent. + CCParams<CryptoContextCKKSRNS> params; + params.SetMultiplicativeDepth(kDepth); + params.SetScalingModSize(50); + params.SetBatchSize(kSlots); + params.SetRingDim(kRingDim); + params.SetScalingTechnique(FIXEDAUTO); + params.SetSecurityLevel(HEStd_NotSet); + auto cc2 = GenCryptoContext(params); + cc2->Enable(PKE); + cc2->Enable(KEYSWITCH); + cc2->Enable(LEVELEDSHE); + + std::istringstream iss(oss.str()); + ASSERT_TRUE(cc2->DeserializeEvalMultKey(iss, SerType::BINARY)); + + // Confirm the original context can still multiply (its keys are still live). + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] * v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + auto res = cc->EvalMult(ct1, ct2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +// --------------------------------------------------------------------------- +// Bootstrap fixture (FIXEDAUTO): depth=25, ringDim=65536, slots=8, UNIFORM_TERNARY +// --------------------------------------------------------------------------- + +class CKKSBootstrapTest : public ::testing::Test { + protected: + static constexpr uint32_t kDepth = 25; + static constexpr uint32_t kRingDim = 1u << 16; + static constexpr uint32_t kSlots = 8; + + CryptoContext<DCRTPoly> cc; + KeyPair<DCRTPoly> keys; + const std::vector<double> v1 = { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8 }; + + void SetUp() override { + CCParams<CryptoContextCKKSRNS> params; + params.SetMultiplicativeDepth(kDepth); + params.SetScalingModSize(50); + params.SetBatchSize(kSlots); + params.SetRingDim(kRingDim); + params.SetScalingTechnique(FIXEDAUTO); + params.SetSecretKeyDist(UNIFORM_TERNARY); + params.SetSecurityLevel(HEStd_NotSet); + if (TestUseCuda()) + params.SetBackend(Backend::CUDA); + cc = GenCryptoContext(params); + cc->Enable(PKE); + cc->Enable(KEYSWITCH); + cc->Enable(LEVELEDSHE); + cc->Enable(ADVANCEDSHE); + cc->Enable(FHE); + keys = cc->KeyGen(); + cc->EvalMultKeyGen(keys.secretKey); + // correctionFactor=11 ensures it exceeds deg=10 (round(log2(q0/2^50)) + // with the default firstModSize=60 used by OpenFHE for FIXEDAUTO). + cc->EvalBootstrapSetup({ 1, 1 }, { 0, 0 }, kSlots, 11); + cc->EvalBootstrapKeyGen(keys.secretKey, kSlots); + if (TestUseCuda()) + cc->LoadContext(keys.publicKey); + } +}; + +// ---- 8. Bootstrap ---- + +TEST_F(CKKSBootstrapTest, DISABLED_Bootstrap) { + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto refreshed = cc->EvalBootstrap(ct); + CheckPrecision(cc, refreshed, keys.secretKey, v1, CKKS_BOOTSTRAP_PRECISION); +} + +TEST_F(CKKSBootstrapTest, DISABLED_BootstrapInPlace) { + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + cc->EvalBootstrapInPlace(ct); + CheckPrecision(cc, ct, keys.secretKey, v1, CKKS_BOOTSTRAP_PRECISION); +} + +// ---- 9. GetPreScaleFactor (FIXEDAUTO) ---- + +TEST_F(CKKSBootstrapTest, DISABLED_GetPreScaleFactorFIXEDAUTO) { + // EvalBootstrapSetup sets m_correctionFactor; without it the result underflows. + double r = cc->GetPreScaleFactor(kSlots); + EXPECT_TRUE(std::isfinite(r)) << "result is not finite: " << r; + EXPECT_GT(r, 0.0); + EXPECT_LT(r, 1.0); +} + +// --------------------------------------------------------------------------- +// Bootstrap fixture (FLEXIBLEAUTO): identical parameters but FLEXIBLEAUTO +// --------------------------------------------------------------------------- + +class CKKSFlexBootstrapTest : public ::testing::Test { + protected: + static constexpr uint32_t kDepth = 25; + static constexpr uint32_t kRingDim = 1u << 16; + static constexpr uint32_t kSlots = 8; + + CryptoContext<DCRTPoly> cc; + KeyPair<DCRTPoly> keys; + + void SetUp() override { + CCParams<CryptoContextCKKSRNS> params; + params.SetMultiplicativeDepth(kDepth); + params.SetScalingModSize(50); + params.SetBatchSize(kSlots); + params.SetRingDim(kRingDim); + params.SetScalingTechnique(FLEXIBLEAUTO); + params.SetSecretKeyDist(UNIFORM_TERNARY); + params.SetSecurityLevel(HEStd_NotSet); + if (TestUseCuda()) + params.SetBackend(Backend::CUDA); + cc = GenCryptoContext(params); + cc->Enable(PKE); + cc->Enable(KEYSWITCH); + cc->Enable(LEVELEDSHE); + cc->Enable(ADVANCEDSHE); + cc->Enable(FHE); + keys = cc->KeyGen(); + cc->EvalMultKeyGen(keys.secretKey); + cc->EvalBootstrapSetup({ 1, 1 }, { 0, 0 }, kSlots, 0); + cc->EvalBootstrapKeyGen(keys.secretKey, kSlots); + if (TestUseCuda()) + cc->LoadContext(keys.publicKey); + } +}; + +// ---- 9. GetPreScaleFactor (FLEXIBLEAUTO) ---- + +TEST_F(CKKSFlexBootstrapTest, DISABLED_GetPreScaleFactorFLEXIBLEAUTO) { + double r = cc->GetPreScaleFactor(kSlots); + EXPECT_TRUE(std::isfinite(r)) << "result is not finite: " << r; + EXPECT_GT(r, 0.0); + // FLEXIBLEAUTO result can exceed 1. +} + +// =========================================================================== +// Additional coverage tests +// =========================================================================== + +// ---- Mutable variants ---- + +TEST_F(CKKSTest, EvalAddMutableCtCt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] + v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + auto res = cc->EvalAddMutable(ct1, ct2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalAddMutableCtPt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] + v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto res = cc->EvalAddMutable(ct1, pt2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalAddMutableInPlaceCtCt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] + v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + cc->EvalAddMutableInPlace(ct1, ct2); + CheckPrecision(cc, ct1, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalSubMutableCtCt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] - v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + auto res = cc->EvalSubMutable(ct1, ct2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalSubMutableCtPt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] - v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto res = cc->EvalSubMutable(ct1, pt2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalSubMutableInPlaceCtCt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] - v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + cc->EvalSubMutableInPlace(ct1, ct2); + CheckPrecision(cc, ct1, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalMultMutableCtCt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] * v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + auto res = cc->EvalMultMutable(ct1, ct2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalMultMutableCtPt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] * v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto res = cc->EvalMultMutable(ct1, pt2); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalMultMutableInPlaceCtCt) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] * v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + cc->EvalMultMutableInPlace(ct1, ct2); + CheckPrecision(cc, ct1, keys.secretKey, expected, CKKS_PRECISION); +} + +TEST_F(CKKSTest, EvalSquareMutable) { + std::vector<double> expected(v2.size()); + for (size_t i = 0; i < v2.size(); ++i) + expected[i] = v2[i] * v2[i]; + auto pt = cc->MakeCKKSPackedPlaintext(v2); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto res = cc->EvalSquareMutable(ct); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +// ---- Multi-level operations ---- + +TEST_F(CKKSTest, MultChainedAllLevels) { + // Multiply through all 5 depth levels, verifying at each step. + auto pt = cc->MakeCKKSPackedPlaintext(v2); + auto ct = cc->Encrypt(pt, keys.publicKey); + std::vector<double> cur = v2; + for (uint32_t d = 0; d < kDepth; ++d) { + ct = cc->EvalMult(ct, ct); + for (size_t i = 0; i < cur.size(); ++i) + cur[i] = cur[i] * cur[i]; + } + // Precision degrades with depth; use a looser tolerance. + CheckPrecision(cc, ct, keys.secretKey, cur, 1e-2); +} + +TEST_F(CKKSTest, RotateAfterMult) { + // Consume a level via multiplication, then rotate at the lower level. + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] * v2[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + auto ct = cc->EvalMult(ct1, ct2); + auto res = cc->EvalRotate(ct, 1); + // Expected: (v1*v2) rotated left by 1. + std::vector<double> rotated(expected.size()); + for (size_t i = 0; i < expected.size(); ++i) + rotated[i] = expected[(i + 1) % expected.size()]; + CheckPrecision(cc, res, keys.secretKey, rotated, CKKS_PRECISION); +} + +// ---- Ciphertext copy and clone ---- + +TEST_F(CKKSTest, CiphertextCopy) { + // Ciphertext<DCRTPoly> is a shared_ptr; copying shares the underlying object. + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + Ciphertext<DCRTPoly> alias(ct); + // Both alias and ct point to the same ciphertext. + cc->EvalAddInPlace(ct, ct); + // alias sees the modification (shared semantics). + std::vector<double> doubled(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + doubled[i] = v1[i] * 2.0; + CheckPrecision(cc, alias, keys.secretKey, doubled, CKKS_PRECISION); +} + +TEST_F(CKKSTest, CiphertextClone) { + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto clone = ct->Clone(); + cc->EvalAddInPlace(ct, ct); + CheckPrecision(cc, clone, keys.secretKey, v1, CKKS_PRECISION); +} + +// ---- Getters / setters ---- + +TEST_F(CKKSTest, GetLevel) { + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + EXPECT_EQ(ct->GetLevel(), 0u); + auto ct2 = cc->EvalMult(ct, ct); + // After mult, level may have increased. + EXPECT_GE(ct2->GetLevel(), 0u); +} + +TEST_F(CKKSTest, GetNoiseScaleDeg) { + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + size_t deg = ct->GetNoiseScaleDeg(); + EXPECT_GE(deg, 1u); +} + +// ---- Automorphism key serialization ---- + +TEST_F(CKKSTest, SerializeDeserializeEvalAutomorphismKey) { + std::ostringstream oss; + ASSERT_TRUE(CryptoContextImpl<DCRTPoly>::SerializeEvalAutomorphismKey(oss, SerType::BINARY)); + EXPECT_GT(oss.str().size(), 0u); + + // Deserialize into a second context with the same parameters. + CCParams<CryptoContextCKKSRNS> params; + params.SetMultiplicativeDepth(kDepth); + params.SetScalingModSize(50); + params.SetBatchSize(kSlots); + params.SetRingDim(kRingDim); + params.SetScalingTechnique(FIXEDAUTO); + params.SetSecurityLevel(HEStd_NotSet); + auto cc2 = GenCryptoContext(params); + cc2->Enable(PKE); + cc2->Enable(KEYSWITCH); + cc2->Enable(LEVELEDSHE); + + std::istringstream iss(oss.str()); + ASSERT_TRUE(cc2->DeserializeEvalAutomorphismKey(iss, SerType::BINARY)); + + // Verify original context can still rotate. + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[(i + 1) % v1.size()]; + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + auto res = cc->EvalRotate(ct, 1); + CheckPrecision(cc, res, keys.secretKey, expected, CKKS_PRECISION); +} + +// ---- EvalAddManyInPlace ---- + +TEST_F(CKKSTest, EvalAddManyInPlace) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[i] + v2[i] + v1[i]; + auto pt1 = cc->MakeCKKSPackedPlaintext(v1); + auto pt2 = cc->MakeCKKSPackedPlaintext(v2); + auto ct1 = cc->Encrypt(pt1, keys.publicKey); + auto ct2 = cc->Encrypt(pt2, keys.publicKey); + auto ct3 = cc->Encrypt(pt1, keys.publicKey); + std::vector<Ciphertext<DCRTPoly>> cts = { ct1, ct2, ct3 }; + cc->EvalAddManyInPlace(cts); + // Result is in cts[0]. + CheckPrecision(cc, cts[0], keys.secretKey, expected, CKKS_PRECISION); +} + +// --------------------------------------------------------------------------- +// 14. Linear transforms: ConvolutionTransform / SpecialConvolutionTransform +// +// The CPU baby-step/giant-step path is compared against an independent +// clear-text simulation of the documented algorithm (which mirrors +// src/CKKS/LinearTransform.cu). Parameters are chosen to cover the branchy +// pieces: single even block, single odd block (linear accumulate), and a +// two-block case (inter-block rotation + even-tree accumulate), plus masking. +// --------------------------------------------------------------------------- + +namespace { + +// Clear-text reference. rot(v, k)[i] = v[(i + k) mod n] (OpenFHE left rotation). +std::vector<double> SimulateConvolution(const std::vector<double>& x, + const std::vector<std::vector<double>>& pts, + const std::vector<int>& indexes, + int bStep, + int gStep, + int stride, + int rowSize, + const std::vector<double>* mask, + int maskRotationStride) { + const int n = static_cast<int>(x.size()); + auto rot = [n](const std::vector<double>& v, int k) { + std::vector<double> out(n); + for (int i = 0; i < n; ++i) + out[i] = v[(((i + k) % n) + n) % n]; + return out; + }; + if (rowSize == 0) + rowSize = bStep * gStep; + + std::vector<std::vector<double>> fr(bStep); + for (int i = 0; i < bStep; ++i) + fr[i] = rot(x, indexes[i]); + + constexpr uint32_t INTERNAL = 8; + uint32_t blockCount = (static_cast<uint32_t>(gStep) + INTERNAL - 1) / INTERNAL; + std::vector<std::vector<double>> blockResults; + for (uint32_t b = 0; b < blockCount; ++b) { + uint32_t start = b * INTERNAL; + uint32_t end = std::min(start + INTERNAL, static_cast<uint32_t>(gStep)); + uint32_t cur = end - start; + std::vector<std::vector<double>> results(cur, std::vector<double>(n, 0.0)); + for (uint32_t j = 0; j < cur; ++j) { + uint32_t gj = start + j; + for (int s = 0; s < n; ++s) + results[j][s] = pts[bStep * gj][s] * fr[0][s]; + for (int i = 1; i < bStep; ++i) { + int ptIdx = bStep * static_cast<int>(gj) + i; + if (ptIdx < rowSize) + for (int s = 0; s < n; ++s) + results[j][s] += pts[ptIdx][s] * fr[i][s]; + } + if (mask != nullptr) { + auto r1 = rot(results[j], maskRotationStride); + auto r2 = rot(results[j], 2 * maskRotationStride); + for (int s = 0; s < n; ++s) + results[j][s] = (results[j][s] + r1[s] + r2[s]) * (*mask)[s]; + } + int rotation = stride * static_cast<int>(cur - j); + if (rotation != 0) + results[j] = rot(results[j], rotation); + } + std::vector<double> acc(n, 0.0); + for (uint32_t j = 0; j < cur; ++j) + for (int s = 0; s < n; ++s) + acc[s] += results[j][s]; + blockResults.push_back(acc); + } + if (blockCount > 1) { + int baseRotation = static_cast<int>(INTERNAL) * stride; + for (uint32_t b = 0; b < blockCount - 1; ++b) { + int rotation = static_cast<int>(blockCount - 1 - b) * baseRotation; + if (rotation != 0) + blockResults[b] = rot(blockResults[b], rotation); + } + } + std::vector<double> out(n, 0.0); + for (const auto& br : blockResults) + for (int s = 0; s < n; ++s) + out[s] += br[s]; + return out; +} + +// Deterministic, per-slot-varying weights so a rotation/index bug cannot hide. +std::vector<std::vector<double>> MakeWeights(int count, int n) { + std::vector<std::vector<double>> w(count, std::vector<double>(n)); + for (int j = 0; j < count; ++j) + for (int s = 0; s < n; ++s) + w[j][s] = 0.1 * (((j * 3 + s) % 7) + 1); + return w; +} + +// Encrypt() takes a Plaintext& lvalue, so bind the encoded plaintext first. +Ciphertext<DCRTPoly> EncryptVec(CryptoContext<DCRTPoly>& cc, const std::vector<double>& v, const PublicKey<DCRTPoly>& pk) { + auto pt = cc->MakeCKKSPackedPlaintext(v); + return cc->Encrypt(pt, pk); +} + +} // namespace + +TEST_F(CKKSTest, ConvolutionTransformSingleBlockEven) { + if (!TestUseCuda()) + GTEST_SKIP() << "CPU convolution is implemented in PR3 (backend-abstraction-cpu-impl)."; + // bStep=2, gStep=2: single block, even-tree accumulate. + const int gStep = 2, bStep = 2, stride = 1; + const std::vector<int> indexes = { 0, 1 }; + auto ptVals = MakeWeights(bStep * gStep, kSlots); + + std::vector<Plaintext> pts; + for (const auto& v : ptVals) + pts.push_back(cc->MakeCKKSPackedPlaintext(v)); + auto ct = EncryptVec(cc, v1, keys.publicKey); + + cc->ConvolutionTransformInPlace(ct, gStep, bStep, pts, indexes, stride); + + auto expected = SimulateConvolution(v1, ptVals, indexes, bStep, gStep, stride, 0, nullptr, 0); + CheckPrecision(cc, ct, keys.secretKey, expected, 1e-3); +} + +TEST_F(CKKSTest, ConvolutionTransformSingleBlockOdd) { + if (!TestUseCuda()) + GTEST_SKIP() << "CPU convolution is implemented in PR3 (backend-abstraction-cpu-impl)."; + // bStep=2, gStep=3: single block, odd count exercises the linear-accumulate branch. + const int gStep = 3, bStep = 2, stride = 1; + const std::vector<int> indexes = { 0, 1 }; + auto ptVals = MakeWeights(bStep * gStep, kSlots); + + std::vector<Plaintext> pts; + for (const auto& v : ptVals) + pts.push_back(cc->MakeCKKSPackedPlaintext(v)); + auto ct = EncryptVec(cc, v1, keys.publicKey); + + cc->ConvolutionTransformInPlace(ct, gStep, bStep, pts, indexes, stride); + + auto expected = SimulateConvolution(v1, ptVals, indexes, bStep, gStep, stride, 0, nullptr, 0); + CheckPrecision(cc, ct, keys.secretKey, expected, 1e-3); +} + +TEST_F(CKKSTest, ConvolutionTransformMultiBlock) { + if (!TestUseCuda()) + GTEST_SKIP() << "CPU convolution is implemented in PR3 (backend-abstraction-cpu-impl)."; + // bStep=1, gStep=9: two giant-step blocks, exercising inter-block rotation + // and accumulation on top of the per-block (size-8 even) accumulate. + const int gStep = 9, bStep = 1, stride = 1; + const std::vector<int> indexes = { 0 }; + auto ptVals = MakeWeights(bStep * gStep, kSlots); + + std::vector<Plaintext> pts; + for (const auto& v : ptVals) + pts.push_back(cc->MakeCKKSPackedPlaintext(v)); + auto ct = EncryptVec(cc, v1, keys.publicKey); + + cc->ConvolutionTransformInPlace(ct, gStep, bStep, pts, indexes, stride); + + auto expected = SimulateConvolution(v1, ptVals, indexes, bStep, gStep, stride, 0, nullptr, 0); + CheckPrecision(cc, ct, keys.secretKey, expected, 5e-3); +} + +TEST_F(CKKSTest, SpecialConvolutionTransform) { + if (!TestUseCuda()) + GTEST_SKIP() << "CPU convolution is implemented in PR3 (backend-abstraction-cpu-impl)."; + // Masked variant: bStep=2, gStep=2, maskRotationStride=1. + const int gStep = 2, bStep = 2, stride = 1, maskRotationStride = 1; + const std::vector<int> indexes = { 0, 1 }; + auto ptVals = MakeWeights(bStep * gStep, kSlots); + std::vector<double> maskVals(kSlots); + for (size_t s = 0; s < kSlots; ++s) + maskVals[s] = (s % 2 == 0) ? 1.0 : 0.5; + + std::vector<Plaintext> pts; + for (const auto& v : ptVals) + pts.push_back(cc->MakeCKKSPackedPlaintext(v)); + auto mask = cc->MakeCKKSPackedPlaintext(maskVals); + auto ct = EncryptVec(cc, v1, keys.publicKey); + + cc->SpecialConvolutionTransformInPlace(ct, gStep, bStep, pts, mask, indexes, stride, maskRotationStride); + + auto expected = SimulateConvolution(v1, ptVals, indexes, bStep, gStep, stride, 0, &maskVals, maskRotationStride); + CheckPrecision(cc, ct, keys.secretKey, expected, 1e-2); +} + +// ---- 15. Additional operation paths ---- + +TEST_F(CKKSTest, EvalFastRotationVector) { + // Vector overload returns one clean rotation per requested index. + const std::vector<int32_t> indices = { 1, 2 }; + auto ct = EncryptVec(cc, v1, keys.publicKey); + auto precomp = cc->EvalFastRotationPrecompute(ct); + uint32_t m = cc->GetCyclotomicOrder(); + auto results = cc->EvalFastRotation(ct, indices, m, precomp); + ASSERT_EQ(results.size(), indices.size()); + for (size_t k = 0; k < indices.size(); ++k) { + std::vector<double> expected(v1.size()); + for (size_t i = 0; i < v1.size(); ++i) + expected[i] = v1[(i + indices[k]) % v1.size()]; + CheckPrecision(cc, results[k], keys.secretKey, expected, CKKS_PRECISION); + } +} + +TEST_F(CKKSTest, EvalFastRotationExt) { + // EvalFastRotationExt yields an extended-basis intermediate (not a plain + // rotation), so just exercise the CPU dispatch and ensure it succeeds. + auto ct = EncryptVec(cc, v1, keys.publicKey); + auto precomp = cc->EvalFastRotationPrecompute(ct); + Ciphertext<DCRTPoly> res; + EXPECT_NO_THROW(res = cc->EvalFastRotationExt(ct, 1, precomp, true)); + EXPECT_NE(res.get(), nullptr); +} + +TEST_F(CKKSTest, AccumulateSumInPlaceWithStart) { + // The 4-arg overload (start, then doubling): start=1 reduces all 8 slots into + // slot 0 (= 36). Like the AccumulateSum siblings, only slot 0 is guaranteed — + // the CUDA backend reduces into slot 0 and leaves the rest 0. + auto pt = cc->MakeCKKSPackedPlaintext(v1); + auto ct = cc->Encrypt(pt, keys.publicKey); + cc->AccumulateSumInPlace(ct, kSlots, 1, 1); + Plaintext out; + cc->Decrypt(ct, keys.secretKey, &out); + out->SetLength(1); + auto vals = out->GetRealPackedValue(); + EXPECT_NEAR(vals[0], 36.0, CKKS_PRECISION); +} + +TEST_F(CKKSTest, SerializeDeserializeContextRoundTrip) { + // Exercises the full-context serializer, including the CPU-only device + // metadata block ("0 { }") written/read by api/Serialize.cpp. + const std::string path = "/tmp/fideslib_cpu_ctx_roundtrip"; + ASSERT_TRUE(Serial::SerializeToFile(path, cc, SerType::BINARY)); + + CryptoContext<DCRTPoly> cc2; + ASSERT_TRUE(Serial::DeserializeFromFile(path, cc2, SerType::BINARY)); + ASSERT_NE(cc2.get(), nullptr); + EXPECT_EQ(cc2->GetRingDimension(), cc->GetRingDimension()); + + std::remove(path.c_str()); + std::remove((path + ".dev").c_str()); +}