Skip to content

Adds Fourier-Extension Functional Bootstrapping - #1276

Open
yspolyakov wants to merge 12 commits into
devfrom
issue1207-ckks-fefbt
Open

Adds Fourier-Extension Functional Bootstrapping#1276
yspolyakov wants to merge 12 commits into
devfrom
issue1207-ckks-fefbt

Conversation

@yspolyakov

@yspolyakov yspolyakov commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fourier-Extension Functional Bootstrapping (FEFBT)

Documentation for PR #1276 — Adds Fourier-Extension Functional Bootstrapping (branch issue1207-ckks-fefbtdev), covering the two feature commits by FYHSSGSS:

Commit Date Summary
fff53d67 2026-07-14 Initial feature: API, CKKS implementation, example, unit tests (+1203 lines across 7 files)
8fc52ae2 2026-08-20 Completion pass: LT mode, FLEXIBLEAUTO* support, configurable lEnc/lDec, depth formula, coefficient reorganization

1. What this feature is

Standard CKKS bootstrapping (EvalBootstrap) refreshes a ciphertext by homomorphically evaluating the identity function — its only job is to remove the q·I overflow left by raising the modulus, via a sine/cosine approximation of modular reduction.

Functional bootstrapping evaluates an arbitrary function f during the refresh, at essentially the cost of one bootstrap plus one polynomial evaluation. OpenFHE already has one flavor (EvalFBT, a digit/LUT-style approach for discrete inputs). FEFBT adds a second flavor aimed at real-valued (continuous) functions:

Approximate f by a truncated Fourier series. Because every Fourier basis function e^(2πi·j·x) is 1-periodic, the unknown integer overflow I introduced by modulus raising vanishes automatically — e^(2πi·j·(x+I)) = e^(2πi·j·x) — so evaluating the series both decrypts homomorphically and applies f in a single pass.

The "Fourier extension" part of the name refers to how the coefficients are produced: f is fitted on a sub-interval of the period (the message domain), with the fit free to do whatever it needs on the rest of the period. This gives much faster-converging series than a plain Fourier expansion of a non-periodic function.

Relationship to existing bootstrapping code

The implementation deliberately mirrors EvalBootstrapStCFirst (slots-to-coefficients before modulus raising) and reuses the existing machinery: CKKSBootstrapPrecom, EvalCoeffsToSlots/EvalSlotsToCoeffs, the linear-transform (LT) fallback, EvalBootstrapKeyGen for keys, and the sparse-encapsulation key-switch path.


2. Pipeline

flowchart TD
    A["Input ciphertext (slots encoding, near bottom of chain)"] --> B["SlotsToCoeffs (or LT when levelBudget = {1,1})"]
    B --> C["Raise modulus to Q0 chain (raw tower-0 reinterpretation)"]
    C --> D["SPARSE_ENCAPSULATED only: two-step sparse key switch"]
    D --> E["AdjustCiphertextFEFBT: fix scaling-factor metadata (FLEXIBLE*)"]
    E --> F["Sparse slots: partial-sum rotations, then scale by 1/(2kN)"]
    F --> G["CoeffsToSlots + conjugate-add (take real part)"]
    G --> H["Chebyshev evaluation of exp(2πi·x / 2^r)"]
    H --> I["r double-angle squarings → z = exp(2πi·x); q·I overflow vanishes by periodicity"]
    I --> J["EvalPoly: series Σ cⱼ·zʲ with user Fourier coefficients"]
    J --> K["+ conjugate + 2·c₀ → f(x), real, refreshed ciphertext"]
Loading

Step-by-step, in FHECKKSRNS::EvalFEFuncBootstrap (ckksrns-fhe.cpp):

  1. StC first. The message is moved to coefficient encoding while still at low level (EvalSlotsToCoeffs, or EvalLinearTransform in LT mode).

  2. Modulus raise. Each polynomial is reduced to its first RNS tower in coefficient format and re-expanded over the full Q0 chain — the standard raw raise, leaving message + q·I.

  3. Sparse encapsulation (if SecretKeyDist == SPARSE_ENCAPSULATED): key-switch to the sparse key before the raise-sensitive steps, then back (automorphism keys 2N−4 / 2N−2), same as regular bootstrapping.

  4. Metadata fix. AdjustCiphertextFEFBT re-stamps the scaling factor from the canonical level table for FLEXIBLEAUTO/FLEXIBLEAUTOEXT (no-op for FIXED*).

  5. Sparse packing. For slots < N/2, partial sums via rotations replicate the sparse message, and the ciphertext was pre-multiplied by 2 to compensate the folding.

  6. Normalization. Multiply by 1/(2kN) — the 1/(2N) from the raise convention plus the 1/K_UNIFORM range compression for uniform keys (for sparse keys the 1/K factor was already folded into the CtS matrix at setup, see §4).

  7. CtS + real part. EvalCoeffsToSlots followed by adding the conjugate.

  8. Complex exponential. A Chebyshev series (key-distribution-specific table, §4) evaluates exp(2πi·x/2^r); r double-angle squarings then produce z = exp(2πi·x) exactly on all branches of the raised message — this is where the integer overflow I disappears.

  9. Fourier series. EvalPoly(ctxtExp, coefficients) evaluates Σⱼ cⱼ·zʲ (with c₀ zeroed out and re-added in the clear), then the conjugate is added and 2·Re(c₀) restored:

    result = 2 · Re( Σⱼ₌₀..d cⱼ · e^(2πi·j·x) )

    The user's coefficient vector must therefore satisfy f(x) = 2·Re(Σ cⱼ e^(2πi·j·x)) on the message domain (see §5 for the convention used by the shipped tables).


3. Public API

Two methods are added at all three layers (CryptoContextImpl, SchemeBase/FHEBase virtuals, FHECKKSRNS overrides):

// cryptocontext.h
void EvalFEFuncBootstrapSetup(std::vector<uint32_t> levelBudget = {5, 4},
                              std::vector<uint32_t> dim1        = {0, 0},
                              uint32_t slots                    = 0);

Ciphertext<Element> EvalFEFuncBootstrap(ConstCiphertext<Element> ciphertext,
                                        std::vector<std::complex<double>> coefficients) const;
  • EvalFEFuncBootstrapSetup(levelBudget, dim1, slots) — precomputes the StC/CtS (or LT) matrices and stores them in the shared m_bootPrecomMap[slots] with BTSlotsEncoding = true. levelBudget = {enc, dec} are the CtS/StC level budgets; {1, 1} selects the single-linear-transform (LT) mode added in the second commit. dim1 is the usual baby-step/giant-step dimension hint; slots = 0 means full packing (N/2).
  • EvalFEFuncBootstrap(ct, coefficients) — performs the pipeline of §2 and returns a refreshed ciphertext encrypting f(message). coefficients are the one-sided Fourier(-extension) coefficients of f.
  • Keys: no new keygen — call the standard EvalBootstrapKeyGen(secretKey, slots) after setup (rotation, conjugation, and sparse-encapsulation keys are shared with regular bootstrapping).

A static depth helper is added to FHECKKSRNS (templated for int64_t and complex<double> coefficient vectors):

static uint32_t GetFEFBTDepth(const std::vector<uint32_t>& levelBudget,
                              const std::vector<VectorDataType>& coefficients,
                              SecretKeyDist skd = SPARSE_TERNARY);

which computes

depth = levelBudget[0] + levelBudget[1]          // CtS + StC
      + depth(Chebyshev exp series for skd)      // ~6–7 levels
      + r(skd)                                   // double-angle squarings
      + depth(EvalPoly of the Fourier series)    // ~⌈log2(deg)⌉ + O(1)

The example adds +6 levels of headroom on top of this for post-bootstrap computation and encoding margin.


4. Supported configurations

Dimension Supported Notes
Key switching HYBRID only throws otherwise (same as regular CKKS bootstrapping)
Scaling technique FIXEDMANUAL, FIXEDAUTO, FLEXIBLEAUTO, FLEXIBLEAUTOEXT FLEXIBLEAUTO* added by the second commit; FLEXIBLEAUTOEXT handled via an extra StC level and an L0 − 1 adjustment
Secret key distribution SPARSE_TERNARY, SPARSE_ENCAPSULATED, UNIFORM_TERNARY selects the exp-approximation table, see below
Word size NATIVE_SIZE = 64 only EvalFEFuncBootstrap throws unconditionally on 128-bit builds; unit tests are compiled out for NATIVEINT == 128
Slots full (N/2) and sparse sparse path uses the ×2 pre-multiplication + partial-sum rotations
Data type tested with COMPLEX CKKS data type in the example the evaluated result is the real part by construction

Per-key-distribution constants (class-scope tables in ckksrns-fhe.h; naming is coeff_exp_<K>_double_<degree> — approximation range [−K, K], then double-angle, Chebyshev degree):

SecretKeyDist Range K Chebyshev table Double-angle iterations r
SPARSE_TERNARY 28 (K_SPARSE) coeff_exp_28_double_48 R_func_28_double_48 = 3
SPARSE_ENCAPSULATED 16 (K_SPARSE_ENCAPSULATED) coeff_exp_16_double_23 R_func_16_double_23 = 4
UNIFORM_TERNARY 512 (K_UNIFORM) coeff_exp_512_double_23 R_func_512_double_23 = 9

The second commit switched the sparse path from K_SPARSE_ALT = 25 to the standard K_SPARSE = 28, aligning FEFBT with the constants used by regular bootstrapping.

At setup time the encoding/decoding matrices are scaled so the pipeline needs no extra level for range compression: scaleEnc = pre/k folds the 1/K factor into CtS for sparse keys (k = 1 for uniform, where the factor is instead applied at runtime in the 1/(2kN) multiplication), and scaleDec handles the q/Δ correction (composite-degree aware). The setup also computes the FLEXIBLEAUTO* correction factor with the same formula as EvalBootstrapSetup.

Level placement of inputs. Because StC runs first, the input ciphertext should sit near the bottom of the chain: both the example and the tests encode at

MakeCKKSPackedPlaintext(input, 1, depth - (levelBudget[1] + 1), nullptr, slots);

i.e., with exactly levelBudget[1] + 1 levels remaining for StC before the raise.


5. Fourier coefficients: convention and provided tables

The runtime computes 2·Re(Σⱼ₌₀..d cⱼ zʲ), so a coefficient vector for a real function f must satisfy:

  • c₀ real, equal to half the DC term of the series;
  • for j ≥ 1, cⱼ is the one-sided coefficient (the c₋ⱼ = conj(cⱼ) half is supplied by the conjugate-add).

Domain normalization. The encrypted message lives in [−0.5, 0.5] (one period). A function on [−B, B] is handled by fitting g(t) = f(2Bt) for t ∈ [−0.5, 0.5] — the example builds inputs in [−0.5, 0.5) and compares against f(2·B·t).

Coefficient tables follow the naming pattern coeff_<function>_<B>_double_<degree> and were moved out of the header by the second commit — the function-specific tables now live where they are used:

Table Function Domain Degree Location
coeff_identity_1_double_25 y = x (baseline) [−0.5, 0.5] 25 ckksrns-fhe.h (shipped baseline)
coeff_exp_2_double_29 exp(x) [−2, 2] 29 example + unit tests
coeff_sigmoid_8_double_34 1/(1+e^(−x)) [−8, 8] 34 example + unit tests
coeff_gelu_8_double_44 GELU (tanh form) [−8, 8] 44 example + unit tests

Per the commit message, the coefficients are generated with a SciPy-based tooling repo (Fourier-extension least-squares fit); the generator itself is not part of this PR.


6. Example

src/pke/examples/FE-functional-bootstrapping-ckks.cpp demonstrates the full flow at production scale:

  • ring dimension 2^16, 2^15 slots (full packing), FLEXIBLEAUTO, 59-bit scaling / 60-bit first modulus, SPARSE_TERNARY, HYBRID with 3 digits, levelBudget = {3, 2};
  • multiplicative depth chosen as max(GetFEFBTDepth(...)) over the three target functions, +6;
  • one setup + keygen, then three EvalFEFuncBootstrap calls on the same input ciphertext (exp, sigmoid, GELU), each reporting total time, amortized per-slot time, sample values, and mean precision in bits.

Sketch:

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

cc->EvalFEFuncBootstrapSetup({3, 2}, {0, 0}, numSlots);
auto keyPair = cc->KeyGen();
cc->EvalMultKeyGen(keyPair.secretKey);
cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlots);

auto ptxt = cc->MakeCKKSPackedPlaintext(input, 1, depth - 3, nullptr, numSlots);
auto ct   = cc->Encrypt(keyPair.publicKey, ptxt);
auto ctSigmoid = cc->EvalFEFuncBootstrap(ct, coeff_sigmoid_8_double_34);

7. Test coverage

src/pke/unittest/utckksrns/UnitTestFEFBT.cpp — parameterized GTest suite (UTCKKSRNS_FEFBT), ring dimension 2^12, depth 26, tolerance eps = 1e-4, 14 cases:

Axis Covered values
Functions sigmoid (primary), exp, GELU-tanh
Key distributions SPARSE_TERNARY, SPARSE_ENCAPSULATED, UNIFORM_TERNARY
Slots full (N/2 = 2048) and sparse (8)
Scaling techniques FIXEDMANUAL, FIXEDAUTO, FLEXIBLEAUTO, FLEXIBLEAUTOEXT
Level budgets {3, 2} and LT mode {1, 1}
Test types FEFBT_ACCURACY (decrypt-and-compare) and FEFBT_POST_ROTATION (rotations on the refreshed ciphertext still work)

The whole suite is compiled out on NATIVEINT == 128 builds.


8. What each commit contributed

fff53d67 — "Add Fourier-Extension CKKS functional bootstrapping"
introduced the complete vertical slice: the two API methods through cryptocontext.hbase-scheme.hbase-fhe.hFHECKKSRNS, the ~280-line CKKS implementation, the example, and the first unit tests.

8fc52ae2 — "feat: complete majority of FEFBT implementation"

  • LT mode: levelBudget = {1, 1} now builds single linear-transform matrices (EvalLinearTransformPrecompute/EvalLinearTransform) instead of the collapsed-FFT decomposition.
  • FLEXIBLEAUTO/FLEXIBLEAUTOEXT support: correction-factor computation in setup, AdjustCiphertextFEFBT metadata fix, L0/lDec adjustments for the AUTOEXT extra level.
  • Configurable lEnc/lDec: encoding levels for the StC/CtS matrices are computed from L0, the level budgets, and the scaling technique instead of being hardcoded; lvlb is now serialized in CKKSBootstrapPrecom.
  • GetFEFBTDepth implemented from the actual polynomial degrees (GetMultiplicativeDepthByCoeffVector) instead of a placeholder.
  • K_SPARSE_ALT → K_SPARSE (25 → 28), matching regular bootstrapping.
  • Coefficient reorganization: function tables moved from the header into the example/tests; identity baseline coeff_identity_1_double_25 added.
  • Touches the existing FBT path: EvalFBTSetupInternal now computes L0, lEnc, lDec with FLEXIBLEAUTOEXT-aware offsets — a behavioral change for the pre-existing EvalFBT feature that deserves its own regression check.

9. Observations for review

These are notes from reading the diff, not part of the feature description:

  1. coeff_identity_1_double_25 sits at namespace scope in ckksrns-fhe.h, after the FHECKKSRNS class closes, but with member-style indentation and a static qualifier (internal linkage — a private copy in every translation unit that includes the header). It looks like it was meant to be a class member alongside the other tables.
  2. Dead code in AdjustCiphertextFEFBT: a ~20-line commented-out alternative implementation (composite-scaling handling) is left in the body. Also, the active body only handles FLEXIBLE*; COMPOSITESCALING* is unaddressed.
  3. Stale error message: the missing-precomputation error in EvalFEFuncBootstrap tells the user to call EvalBootstrapSetup/EvalBootstrapKeyGen rather than EvalFEFuncBootstrapSetup. A nearby exception also uses the abbreviation "FEFBS" while everything else says FEFBT.
  4. 128-bit guard inconsistency: EvalFEFuncBootstrapSetup permits 128-bit builds under FIXED* scaling, but EvalFEFuncBootstrap throws unconditionally on NATIVEINT == 128 — the setup guard could reject earlier.
  5. Shared precomputation map: setup writes to the same m_bootPrecomMap[slots] used by EvalBootstrapSetup; configuring both regular bootstrapping and FEFBT for the same slot count on one context silently overwrites one with the other.
  6. Pass-by-value coefficient plumbing: std::vector<std::complex<double>> is copied through all four layers on every call (the mutation of coefficients[0] justifies one copy at the innermost layer, not four).
  7. #include <chrono> was added to ckksrns-fhe.cpp but the timing code uses the existing TimeVar/TIC/TOC; the include appears unnecessary.
  8. Interaction with issue-1218 FBT work: item 8's EvalFBTSetupInternal change overlaps the recent FLEXIBLE* FBT support; worth running the FBT unit tests with FLEXIBLEAUTOEXT on this branch.

yspolyakov and others added 11 commits May 21, 2025 12:28
Updated documentation in security.rs for v1.5.0
Added funding acknowledgment for ARPA-H research.
- populate scipy coefficient generation repo
- move coefficients from header to examples/unit tests
- add identity function baseline coefficients
- implement GetFEFBTDepth based on polynomial degree
- switch K_SPARSE_ALT to K_SPARSE
- add LT mode
- make lEnc/lDec configurable, remove hardcode
- add FLEXIBLEAUTO* support
@yspolyakov yspolyakov added this to the Release 1.6.0 milestone Aug 21, 2026
@yspolyakov yspolyakov self-assigned this Aug 21, 2026
@yspolyakov yspolyakov added the new feature New feature or request label Aug 21, 2026
@yspolyakov

Copy link
Copy Markdown
Contributor Author

The below discussion compares the FLEXIBLEAUTO implementation with the one in the issue-1218 branch. This will be addressed after issue-1218 is merged to dev.

FLEXIBLEAUTO handling: comparison with the issue-1218 FBT design

I compared the FLEXIBLE* support here against the approach recently added for EvalFBT on issue-1218 (commit a1204635). The two share the same core idea — after ModRaise, declare the raised ciphertext onto the FLEXIBLE table-SF chain by re-stamping its scaling factor — but this PR implements only half of that design. Issue-1218 is "declaration + fold": the declaration is made exact by folding the scale ratios into the encoding/decoding matrices. FEFBT has the declaration with no fold, which works only because it silently relies on the scaling primes being very close to 2^p.

Side by side

Issue-1218 (EvalFBT):

  • After raise: raised->SetScalingFactor(GetScalingFactorReal(raised->GetLevel())) — the declaration.
  • At setup: scaleEnc *= SF(raisedLevel) / 2^p and scaleDec *= 2^p / SF(finalLevel) — the fold that makes the declaration exact, regardless of how far the primes sit from 2^p. No extra level; the depth budget is identical across all scaling modes.
  • FLEXIBLEAUTOEXT: raised basis pops the ext modulus and the input is imported one level deeper (extOff = 1).
  • AUTO modes use internal rescaling (ModReduceInternalInPlace), because public ModReduce is a no-op outside FIXEDMANUAL.
  • No correction factor needed anywhere.

This PR (EvalFEFuncBootstrap):

  • AdjustCiphertextFEFBT does the same declaration (re-stamp from the table) — and nothing else.
  • No fold: scaleEnc = pre/k, scaleDec = 1/pre contain no SF-ratio terms.
  • AUTOEXT: pops the ext modulus and adds +1 to lDec — equivalent in spirit to extOff = 1; this part matches.
  • Setup computes m_correctionFactor (EvalBootstrap-style), but EvalFEFuncBootstrap never uses it.

Why the bare declaration still passes the tests: unlike FBT, which imports an external RLWE ciphertext at an arbitrary initialScaling (ratio far from 1, so the fold is mandatory), FEFBT's raise boundary is internal — the StC output's true scale is the table SF at the bottom level, and for standard single-prime FLEXIBLE chains every table SF is within ~2⁻⁴⁵ of 2^p, so the declared/true mismatch is ~2⁻⁴⁰: invisible next to CKKS noise. The approaches differ in principle, but the numerical gap only opens when the primes stray from 2^p.

Suggested changes

  1. Fix the correction-factor clobber (real bug). EvalFEFuncBootstrapSetup unconditionally overwrites the shared member m_correctionFactor — which FEFBT never reads, but regular EvalBootstrap does. It also uses the old formula (−0.265·(...)+19.1, clamp 7–13), while EvalBootstrapSetup on this branch now uses the newer spreadsheet-fit, BTSlotsEncoding-aware formulas (clamp 7–14 / 6–13). Calling EvalFEFuncBootstrapSetup after EvalBootstrapSetup on the same context silently degrades regular bootstrapping precision. Since the FEFBT design needs no correction factor (same conclusion as issue-1218), please delete the computation entirely.

  2. Adopt the fold. All FEFBT levels are static at setup time (the StC bottom level and the raised level are fixed by the pipeline), so folding the true-scale/declared-scale ratio into scaleEnc is a one-line setup change, exactly as issue-1218 did. For standard FLEXIBLE chains this changes nothing measurable — its value is that it removes the hidden "primes ≈ 2^p" assumption and is the prerequisite for COMPOSITESCALING support, where prime deviations are large. (The commented-out code in AdjustCiphertextFEFBT is an attempted composite-aware EvalMult adjustment that would burn a level; the fold gets the same effect for free.) With the fold in place, the dead code can be removed and AdjustCiphertextFEFBT stays a pure declaration.

  3. Align the double-angle loop with the internal-rescale pattern. The loop does cc->EvalSquareInPlace + public cc->ModReduceInPlace; the latter is a no-op for all AUTO modes, so correctness currently rides on EvalSquare's lazy auto-rescale. That happens to preserve the table-SF invariant (the tests confirm it), but EvalBootstrap and the issue-1218 FBT path both use algo->ModReduceInternalInPlace here deliberately — worth matching for consistency and to keep the noise-degree states predictable.

  4. Consider porting the noise-measurement tests. The suite currently asserts eps = 1e-4 accuracy only. Issue-1218 added FBT_NOISE cases with a MeasureNoiseBits helper and asserted that FLEXIBLE* beats FIXEDMANUAL (~6 bits for the binary LUT). The same instrumentation would show whether FLEXIBLE* is actually buying precision in FEFBT or just passing — the Chebyshev-exp approximation floor can mask the difference, exactly as we found on the FBT side.

One thing that does not need changing: the AUTOEXT one-level-deeper handling — lDec + (st == FLEXIBLEAUTOEXT) plus the ext-modulus pop already mirrors the issue-1218 extOff design, and test cases 13–14 cover it.

- Clarify FEFBT identity coefficient declaration
- Remove obsolete composite-scaling code from AdjustCiphertextFEFBT
- Correct FEFBT setup guidance in precomputation errors
- Reject FEFBT setup on unsupported 128-bit builds
- Remove the unused chrono header
@FYHSSGSS FYHSSGSS self-assigned this Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Fourier-Extension CKKS functional bootstrapping from 2026/367

3 participants