Conversation
…Sum, Horner CtS/StC, and scheme-switching linear transforms
yspolyakov
approved these changes
Jul 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR makes OpenFHE's CKKS rotation-heavy paths — bootstrap EvalPartialSumInPlace, the Horner giant-step in CtS/StC, and the scheme-switching linear transforms — deterministic across entry points and cheaper, by (1) routing every automorphism through one shared core and (2) keeping rotation-fold accumulators in the extended Q·P basis with a single deferred ApproxModDown per fold. It also parameterizes the partial-sum fold radix so the performance/rotation-key-count trade-off is explicit.
Motivation: GPU CKKS backends (specifically FIDESlib) implement rotations in hoisted, lazily-accumulated form. For a GPU backend to produce ciphertexts bit-identical to the CPU reference, the CPU paths must apply the same operation sequence with the same rounding. The changes here achieve that while strictly reducing CPU-side mod-down counts — they are not GPU-specific and stand on their own as a CPU improvement.
Commits
Unification of EvalFastRotation/EvalRotate/EvalAutomorphism/Conjugate
Adds LeveledSHEBase::EvalAutomorphismCore(ciphertext, autoIndex, digits, evalKey) — the automorphism-plus-key-switch step evaluated from precomputed digits — and delegates EvalFastRotation, EvalAutomorphism (hence EvalRotate/EvalAtIndex), and CKKS Conjugate to it. Previously EvalAutomorphism used a non-hoisted sequence (KeySwitchInPlace, then permute both components) whose rounding differs from the hoisted path; the two entry points produced bit-different results for the same rotation. Now every rotation entry point is bit-identical to EvalFastRotation with fresh digits.
Lazy extended-basis accumulation (deferred ApproxModDown)
Rotation folds previously settled each key-switched rotation back to the base ring before adding it to the accumulator. Two exact identities make that unnecessary:
adding a base-ring operand into an extended-basis (Q·P) accumulator lifts it by P mod q exactly, and ApproxModDown(P·x + ks) = x + ApproxModDown(ks) — no rounding difference;
automorphisms are coefficient permutations (exact, linear), so they commute with the deferred settle.
So a fold of k rotations can keep the running sum extended and perform one ApproxModDown at the end, bit-identical to the eager form. Applied to:
EvalPartialSumInPlace (bootstrap partial sums) — the accumulator settles once per fold; the c1 component still settles per step, since the next step's digit decomposition consumes it in the base ring;
the Horner giant-step in CoeffsToSlots/SlotsToCoeffs (EvalHornerGiantRotate), including folding the delta corrections in before the final settle;
scheme-switching EvalLTWithPrecomputeSwitch/EvalLTRectWithPrecomputeSwitch (new defaulted ext flag; the sparse SlotsToCoeffs doubling and the wide-matrix log-fold now accumulate extended and settle once).
ApproxModDown is the dominant cost of a rotation after the key-switch products, so per fold this removes all but one of them on the accumulator path.
Generalizes the partial-sum fold into a radix-r rotation fold and adds two overloads:
EvalPartialSumInPlace(ct, stride, size) — radix-2 specialization, used by all in-tree callers;
EvalPartialSumInPlace(ct, stride, size, radix) — general form (delegates to the 3-arg version when radix == 2).
The trade-off the parameter exposes: mod-ups per fold scale as log2(size)/log2(r), while dedicated rotation keys grow as (r−1)/log2(r) · log2(size). Radix 2 is the default because its rotation indices are all powers of two — shared with the keys bootstrapping already generates — so it minimizes key storage at zero incremental key cost.