From 529dcd537d92abdadfbc019af060cb8c19f0bd75 Mon Sep 17 00:00:00 2001 From: lehugueni Date: Tue, 19 Nov 2024 15:30:17 +0100 Subject: [PATCH 1/6] fix innersum bgv --- schemes/bgv/evaluator.go | 37 +++++++++++++++++++++++++++++++++++++ schemes/bgv/params.go | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/schemes/bgv/evaluator.go b/schemes/bgv/evaluator.go index 554f7c5f3..fb94c00c0 100644 --- a/schemes/bgv/evaluator.go +++ b/schemes/bgv/evaluator.go @@ -1505,6 +1505,43 @@ func (eval Evaluator) RotateHoistedLazyNew(level int, rotations []int, op0 *rlwe return } +// InnerSum computes the inner sum of the underlying slots (see [rlwe.Evaluator.InnerSum]). +// NB: in the slot encoding of BGV/BFV, the underlying N slots are arranged as 2 rows of N/2 slots. +// If n*batchSize < N/2, InnerSum computes the [rlwe.Evaluator.InnerSum] of each row separately. +// If n*batchSize = N, InnerSum computes the [rlwe.Evaluator.InnerSum] on the concatenation of both rows. +// NOTE: In this case, InnerSum performs an addition and a [Evaluator.RotateRowsNew] on top +// Otherwise, InnerSum returns an error. +func (eval Evaluator) InnerSum(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *rlwe.Ciphertext) (err error) { + N := eval.parameters.N() + halfN := N >> 1 + l := n * batchSize + + if l > halfN { + if l != N { + return fmt.Errorf("innersum: n*batchSize=%d > N/2=%d and n*batchSize != N=%d", l, halfN, N) + } + + if err = eval.Evaluator.InnerSum(ctIn, batchSize, n/2, opOut); err != nil { + return + } + + var ctRot *rlwe.Ciphertext + ctRot, err = eval.RotateRowsNew(opOut) + if err != nil { + return + } + + if err = eval.Add(opOut, ctRot, opOut); err != nil { + return + } + + return + } + + err = eval.Evaluator.InnerSum(ctIn, batchSize, n, opOut) + return +} + // MatchScalesAndLevel updates the both input ciphertexts to ensures that their scale matches. // To do so it computes t0 * a = opOut * b such that: // - ct0.Scale * a = opOut.Scale: make the scales match. diff --git a/schemes/bgv/params.go b/schemes/bgv/params.go index 066263837..8ae690de7 100644 --- a/schemes/bgv/params.go +++ b/schemes/bgv/params.go @@ -257,7 +257,7 @@ func (p Parameters) GaloisElementForRowRotation() uint64 { // InnerSum operation with parameters batch and n. func (p Parameters) GaloisElementsForInnerSum(batch, n int) (galEls []uint64) { galEls = rlwe.GaloisElementsForInnerSum(p, batch, n) - if n > p.N()>>1 { + if n*batch > p.N()>>1 { galEls = append(galEls, p.GaloisElementForRowRotation()) } return From 141c402c9134739b22f1d9bf17f56fe46cac2672 Mon Sep 17 00:00:00 2001 From: lehugueni Date: Thu, 21 Nov 2024 09:26:19 +0100 Subject: [PATCH 2/6] tests + use maxslots instead of N --- schemes/bgv/bgv_test.go | 75 ++++++++++++++++++++++++++++++++++++++++ schemes/bgv/evaluator.go | 16 +++------ schemes/bgv/params.go | 2 +- 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/schemes/bgv/bgv_test.go b/schemes/bgv/bgv_test.go index 0dca91dd0..e46ddbad3 100644 --- a/schemes/bgv/bgv_test.go +++ b/schemes/bgv/bgv_test.go @@ -14,6 +14,7 @@ import ( "github.com/tuneinsight/lattigo/v6/core/rlwe" "github.com/tuneinsight/lattigo/v6/ring" + "github.com/tuneinsight/lattigo/v6/utils" ) var flagPrintNoise = flag.Bool("print-noise", false, "print the residual noise") @@ -665,6 +666,80 @@ func testEvaluatorBvg(tc *TestContext, t *testing.T) { } }) } + + // Naive implementation of the inner sum for reference + innersum := func(values []uint64, n, batchSize int) { + tmp := make([]uint64, len(values)) + copy(tmp, values) + for i := 1; i < n; i++ { + rot := utils.RotateSlice(tmp, i*batchSize) + for j := range values { + values[j] = (values[j] + rot[j]) % tc.Params.PlaintextModulus() + } + } + } + + for _, N := range []int{tc.Params.N(), tc.Params.MaxSlots()} { + for _, lvl := range testLevel { + t.Run(name("Evaluator/InnerSum/N slots", tc, lvl), func(t *testing.T) { + if lvl == 0 { + t.Skip("Skipping: Level = 0") + } + n := N >> 2 + batchSize := 1 << 2 + + galEls := tc.Params.GaloisElementsForInnerSum(batchSize, n) + evl := tc.Evl.WithKey(rlwe.NewMemEvaluationKeySet(nil, tc.Kgen.GenGaloisKeysNew(galEls, tc.Sk)...)) + + want, _, ciphertext0 := NewTestVector(tc.Params, tc.Ecd, tc.Enc, lvl, tc.Params.NewScale(3)) + + innersum(want, n, batchSize) + + receiver := NewCiphertext(tc.Params, 1, lvl) + + require.NoError(t, evl.InnerSum(ciphertext0, batchSize, n, receiver)) + + have := make([]uint64, len(want)) + require.NoError(t, tc.Ecd.Decode(tc.Dec.DecryptNew(receiver), have)) + + for i := 0; i < len(want); i += n * batchSize { + require.Equal(t, want[i:i+batchSize], have[i:i+batchSize]) + } + }) + } + } + + for _, lvl := range testLevel { + t.Run(name("Evaluator/InnerSum/N/2 slots", tc, lvl), func(t *testing.T) { + if lvl == 0 { + t.Skip("Skipping: Level = 0") + } + n := 7 + batchSize := 13 + l := n * batchSize + halfN := tc.Params.MaxSlots() >> 1 + + galEls := tc.Params.GaloisElementsForInnerSum(batchSize, n) + evl := tc.Evl.WithKey(rlwe.NewMemEvaluationKeySet(nil, tc.Kgen.GenGaloisKeysNew(galEls, tc.Sk)...)) + + want, _, ciphertext0 := NewTestVector(tc.Params, tc.Ecd, tc.Enc, lvl, tc.Params.NewScale(3)) + + innersum(want[:halfN], n, batchSize) + innersum(want[halfN:], n, batchSize) + + receiver := NewCiphertext(tc.Params, 1, lvl) + + require.NoError(t, evl.InnerSum(ciphertext0, batchSize, n, receiver)) + + have := make([]uint64, len(want)) + require.NoError(t, tc.Ecd.Decode(tc.Dec.DecryptNew(receiver), have)) + + for i, j := 0, halfN; i < halfN; i, j = i+l, j+l { + require.Equal(t, want[i:i+batchSize], have[i:i+batchSize]) + require.Equal(t, want[j:j+batchSize], have[j:j+batchSize]) + } + }) + } } func testEvaluatorBfv(tc *TestContext, t *testing.T) { diff --git a/schemes/bgv/evaluator.go b/schemes/bgv/evaluator.go index fb94c00c0..7ab6a4441 100644 --- a/schemes/bgv/evaluator.go +++ b/schemes/bgv/evaluator.go @@ -1507,20 +1507,14 @@ func (eval Evaluator) RotateHoistedLazyNew(level int, rotations []int, op0 *rlwe // InnerSum computes the inner sum of the underlying slots (see [rlwe.Evaluator.InnerSum]). // NB: in the slot encoding of BGV/BFV, the underlying N slots are arranged as 2 rows of N/2 slots. -// If n*batchSize < N/2, InnerSum computes the [rlwe.Evaluator.InnerSum] of each row separately. -// If n*batchSize = N, InnerSum computes the [rlwe.Evaluator.InnerSum] on the concatenation of both rows. -// NOTE: In this case, InnerSum performs an addition and a [Evaluator.RotateRowsNew] on top -// Otherwise, InnerSum returns an error. +// If n*batchSize is a multiple of N, InnerSum computes the [rlwe.Evaluator.InnerSum] on the N slots. +// NOTE: In this case, InnerSum performs an addition and a [Evaluator.RotateRowsNew] on top. +// Otherwise, InnerSum computes the [rlwe.Evaluator.InnerSum] of each row separately. func (eval Evaluator) InnerSum(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *rlwe.Ciphertext) (err error) { - N := eval.parameters.N() - halfN := N >> 1 + N := eval.parameters.MaxSlots() l := n * batchSize - if l > halfN { - if l != N { - return fmt.Errorf("innersum: n*batchSize=%d > N/2=%d and n*batchSize != N=%d", l, halfN, N) - } - + if l%N == 0 { if err = eval.Evaluator.InnerSum(ctIn, batchSize, n/2, opOut); err != nil { return } diff --git a/schemes/bgv/params.go b/schemes/bgv/params.go index 8ae690de7..84d405096 100644 --- a/schemes/bgv/params.go +++ b/schemes/bgv/params.go @@ -257,7 +257,7 @@ func (p Parameters) GaloisElementForRowRotation() uint64 { // InnerSum operation with parameters batch and n. func (p Parameters) GaloisElementsForInnerSum(batch, n int) (galEls []uint64) { galEls = rlwe.GaloisElementsForInnerSum(p, batch, n) - if n*batch > p.N()>>1 { + if n*batch%p.MaxSlots() == 0 { galEls = append(galEls, p.GaloisElementForRowRotation()) } return From b67c72b7bada102ea69debf87399e38f9287e4a7 Mon Sep 17 00:00:00 2001 From: lehugueni Date: Thu, 21 Nov 2024 10:06:00 +0100 Subject: [PATCH 3/6] return ctIn when n=1, batchSize=k*N --- schemes/bgv/evaluator.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/schemes/bgv/evaluator.go b/schemes/bgv/evaluator.go index 7ab6a4441..7b69557a8 100644 --- a/schemes/bgv/evaluator.go +++ b/schemes/bgv/evaluator.go @@ -1515,6 +1515,13 @@ func (eval Evaluator) InnerSum(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *r l := n * batchSize if l%N == 0 { + if n == 1 { + if ctIn != opOut { + opOut.Copy(ctIn) + } + return + } + if err = eval.Evaluator.InnerSum(ctIn, batchSize, n/2, opOut); err != nil { return } From 3a52e44be662307cceb2940762ff6410b83ed3b9 Mon Sep 17 00:00:00 2001 From: lehugueni Date: Fri, 22 Nov 2024 13:19:07 +0100 Subject: [PATCH 4/6] apply feedback + add test cases + update changelog --- CHANGELOG.md | 6 ++ core/rlwe/inner_sum.go | 33 ++---- core/rlwe/rlwe_test.go | 5 +- examples/singleparty/tutorials/ckks/main.go | 42 ++++++-- schemes/bgv/bgv_test.go | 113 +++++++++++--------- schemes/bgv/evaluator.go | 74 ++++++++++--- schemes/bgv/params.go | 2 +- schemes/ckks/evaluator.go | 48 +++++++++ 8 files changed, 225 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ebd36211..1327de57b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog All notable changes to this library are documented in this file. +## [6.x.x] - 16.12.2024 +- Refactoring of the InnerSum methods: + - `rlwe.Evaluator.InnerSum` has been replaced by `rlwe.Evaluator.PartialTrace` + - Introduction of the `bgv.Evaluator.InnerSum` and `ckks.Evaluator.InnerSum` methods, which have the same behaviour as the old `InnerSum` method for parameters `n` and `batchSize` s.t. `n*batchSize` divides the number of slots. Parameters not satisfying this condition are rejected. + - Introduction of the `bgv.Evaluator.RotateAndAdd` and `ckks.Evaluator.RotateAndAdd` methods, which have the same behaviour as the old `InnerSum` method for all parameters. + ## [6.1.0] - 04.10.2024 - Update of `PrecisionStats` in `ckks/precision.go`: - The precision is now computed as the min/max/average/... of the log of the error (instead of the log of the min/max/average/... of the error). diff --git a/core/rlwe/inner_sum.go b/core/rlwe/inner_sum.go index 2e588b253..39c395b10 100644 --- a/core/rlwe/inner_sum.go +++ b/core/rlwe/inner_sum.go @@ -144,26 +144,13 @@ func GaloisElementsForTrace(params ParameterProvider, logN int) (galEls []uint64 return } -// InnerSum applies an optimized inner sum on the Ciphertext (log2(n) + HW(n) rotations with double hoisting). -// The operation assumes that `ctIn` encrypts Slots/`batchSize` sub-vectors of size `batchSize` and will add them together (in parallel) in groups of `n`. -// It outputs in opOut a [Ciphertext] for which the "leftmost" sub-vector of each group is equal to the sum of the group. -// -// The inner sum is computed in a tree fashion. Example for batchSize=2 & n=4 (garbage slots are marked by 'x'): -// -// 1. [{a, b}, {c, d}, {e, f}, {g, h}, {a, b}, {c, d}, {e, f}, {g, h}] -// -// 2. [{a, b}, {c, d}, {e, f}, {g, h}, {a, b}, {c, d}, {e, f}, {g, h}] -// + -// [{c, d}, {e, f}, {g, h}, {x, x}, {c, d}, {e, f}, {g, h}, {x, x}] (rotate batchSize * 2^{0}) -// = -// [{a+c, b+d}, {x, x}, {e+g, f+h}, {x, x}, {a+c, b+d}, {x, x}, {e+g, f+h}, {x, x}] -// -// 3. [{a+c, b+d}, {x, x}, {e+g, f+h}, {x, x}, {a+c, b+d}, {x, x}, {e+g, f+h}, {x, x}] (rotate batchSize * 2^{1}) -// + -// [{e+g, f+h}, {x, x}, {x, x}, {x, x}, {e+g, f+h}, {x, x}, {x, x}, {x, x}] = -// = -// [{a+c+e+g, b+d+f+h}, {x, x}, {x, x}, {x, x}, {a+c+e+g, b+d+f+h}, {x, x}, {x, x}, {x, x}] -func (eval Evaluator) InnerSum(ctIn *Ciphertext, batchSize, n int, opOut *Ciphertext) (err error) { +// PartialTrace applies a partial trace on the input ciphertext with the automorphisms phi(i*offset, X), 0 <= i < n, where phi(k, X): X -> X^{5^k} +// i.e. opOut = \sum_{i = 0}^{n-1} phi(i*offset, ctIn). +// At the scheme level, this function is used to perform inner sums or efficiently replicate slots. +func (eval Evaluator) PartialTrace(ctIn *Ciphertext, offset, n int, opOut *Ciphertext) (err error) { + if n == 0 || offset == 0 { + return fmt.Errorf("partialtrace: invalid parameter (n = 0 or batchSize = 0)") + } params := eval.GetRLWEParameters() @@ -236,7 +223,7 @@ func (eval Evaluator) InnerSum(ctIn *Ciphertext, batchSize, n int, opOut *Cipher if j&1 == 1 { k := n - (n & ((2 << i) - 1)) - k *= batchSize + k *= offset // If the rotation is not zero if k != 0 { @@ -281,7 +268,7 @@ func (eval Evaluator) InnerSum(ctIn *Ciphertext, batchSize, n int, opOut *Cipher if !state { - rot := params.GaloisElement((1 << i) * batchSize) + rot := params.GaloisElement((1 << i) * offset) // ctInNTT = ctInNTT + Rotate(ctInNTT, 2^i) if err = eval.AutomorphismHoisted(levelQ, ctInNTT, eval.BuffDecompQP, rot, cQ); err != nil { @@ -486,7 +473,7 @@ func GaloisElementsForInnerSum(params ParameterProvider, batch, n int) (galEls [ // two consecutive sub-vectors to replicate. // This method is faster than Replicate when the number of rotations is large and it uses log2(n) + HW(n) instead of n. func (eval Evaluator) Replicate(ctIn *Ciphertext, batchSize, n int, opOut *Ciphertext) (err error) { - return eval.InnerSum(ctIn, -batchSize, n, opOut) + return eval.PartialTrace(ctIn, -batchSize, n, opOut) } // GaloisElementsForReplicate returns the list of Galois elements necessary to perform the diff --git a/core/rlwe/rlwe_test.go b/core/rlwe/rlwe_test.go index f76dddf35..da09828b4 100644 --- a/core/rlwe/rlwe_test.go +++ b/core/rlwe/rlwe_test.go @@ -1083,7 +1083,7 @@ func testSlotOperations(tc *TestContext, level, bpw2 int, t *testing.T) { enc := tc.enc dec := tc.dec - t.Run(testString(params, level, params.MaxLevelP(), bpw2, "Evaluator/InnerSum"), func(t *testing.T) { + t.Run(testString(params, level, params.MaxLevelP(), bpw2, "Evaluator/PartialTrace"), func(t *testing.T) { if params.MaxLevelP() == -1 { t.Skip("test requires #P > 0") @@ -1095,6 +1095,7 @@ func testSlotOperations(tc *TestContext, level, bpw2 int, t *testing.T) { ringQ := tc.params.RingQ().AtLevel(level) pt := genPlaintext(params, level, 1<<30) + pt.LogDimensions = ring.Dimensions{Rows: 1, Cols: params.logN - 1} ptInnerSum := *pt.Value.CopyNew() ct, err := enc.EncryptNew(pt) require.NoError(t, err) @@ -1102,7 +1103,7 @@ func testSlotOperations(tc *TestContext, level, bpw2 int, t *testing.T) { // Galois Keys evk := NewMemEvaluationKeySet(nil, kgen.GenGaloisKeysNew(GaloisElementsForInnerSum(params, batch, n), sk)...) - require.NoError(t, eval.WithKey(evk).InnerSum(ct, batch, n, ct)) + require.NoError(t, eval.WithKey(evk).PartialTrace(ct, batch, n, ct)) dec.Decrypt(ct, pt) diff --git a/examples/singleparty/tutorials/ckks/main.go b/examples/singleparty/tutorials/ckks/main.go index 40b441598..b24937a44 100644 --- a/examples/singleparty/tutorials/ckks/main.go +++ b/examples/singleparty/tutorials/ckks/main.go @@ -590,13 +590,13 @@ func main() { // The `circuits/lintrans` package provides a multiple handy linear transformations. // We will start with the inner sum. - // Thus method allows to aggregate `n` sub-vectors of size `batch`. - // For example given a vector [x0, x1, x2, x3, x4, x5, x6, x7], batch = 2 and n = 3 - // it will return the vector [x0+x2+x4, x1+x3+x5, x2+x4+x6, x3+x5+x7, x4+x6+x0, x5+x7+x1, x6+x0+x2, x7+x1+x3] - // Observe that the inner sum wraps around the vector, this behavior must be taken into account. + // This method allows to aggregate `n` sub-vectors of size `batch` and it stores the result in the leftmost sub-vector of each "group". + // For example given a vector [x0, x1, x2, x3, x4, x5, x6, x7], batch = 2 and n = 4 + // it will return the vector [x0+x2+x4+x6, x1+x3+x5+x7, X, X, X, X, X, X], where X marks garbage slots. + // Note that n*batch must divide the length of the vector (i.e. the number of slots). - batch := 37 - n := 127 + batch := 32 + n := 128 // The innersum operations is carried out with log2(n) + HW(n) automorphisms and we need to // generate the corresponding Galois keys and provide them to the `Evaluator`. @@ -619,7 +619,35 @@ func main() { // apply the innersum and then only apply the rescaling. fmt.Printf("Innersum %s", ckks.GetPrecisionStats(params, ecd, dec, want, res, 0, false).String()) - // The replicate operation is exactly the same as the innersum operation, but in reverse + // Sometimes we wish to compute an inner sum on the first values of the vector only. + // In this case, n*batch does not necessarily divide the length of the vector and the RotateAndAdd function must be used instead. + // This method allows to repeatedly shift the vector by batch values and add (i.e. \sum_{i=0}^{n-1} v << (i*batch), where v is the input vector). + // For example given a vector [x0, x1, x2, x3, x4, x5, x6, x7], batch = 2 and n = 3 + // it will return the vector [x0+x2+x4, x1+x3+x5, x2+x4+x6, x3+x5+x7, x4+x6+x0, x5+x7+x1, x6+x0+x2, x7+x1+x3]. + // Observe that the inner sum wraps around the vector, this behavior must be taken into account. + + batch = 37 + n = 127 + eval = eval.WithKey(rlwe.NewMemEvaluationKeySet(rlk, kgen.GenGaloisKeysNew(params.GaloisElementsForInnerSum(batch, n), sk)...)) + + // Plaintext circuit + copy(want, values1) + for i := 1; i < n; i++ { + for j, vi := range utils.RotateSlice(values1, i*batch) { + want[j] += vi + } + } + + if err := eval.RotateAndAdd(ct1, batch, n, res); err != nil { + panic(err) + } + + // Note that this method can obviously be used to average values. + // For a good noise management, it is recommended to first multiply the values by 1/n, then + // apply the inner sum and then only apply the rescaling. + fmt.Printf("RotateAndAdd %s", ckks.GetPrecisionStats(params, ecd, dec, want, res, 0, false).String()) + + // The replicate operation is exactly the same as the rotate and add operation, but in reverse eval = eval.WithKey(rlwe.NewMemEvaluationKeySet(rlk, kgen.GenGaloisKeysNew(params.GaloisElementsForReplicate(batch, n), sk)...)) // Plaintext circuit diff --git a/schemes/bgv/bgv_test.go b/schemes/bgv/bgv_test.go index e46ddbad3..26768979e 100644 --- a/schemes/bgv/bgv_test.go +++ b/schemes/bgv/bgv_test.go @@ -668,77 +668,92 @@ func testEvaluatorBvg(tc *TestContext, t *testing.T) { } // Naive implementation of the inner sum for reference - innersum := func(values []uint64, n, batchSize int) { - tmp := make([]uint64, len(values)) - copy(tmp, values) + innersum := func(values []uint64, n, batchSize int, rotateAndAdd bool) { + aggregate := false + if n*batchSize == len(values) && !rotateAndAdd { + aggregate = true + n = n / 2 + } + halfN := len(values) >> 1 + tmp1 := make([]uint64, halfN) + tmp2 := make([]uint64, halfN) + copy(tmp1, values[:halfN]) + copy(tmp2, values[halfN:]) for i := 1; i < n; i++ { - rot := utils.RotateSlice(tmp, i*batchSize) - for j := range values { - values[j] = (values[j] + rot[j]) % tc.Params.PlaintextModulus() + rot1 := utils.RotateSlice(tmp1, i*batchSize) + rot2 := utils.RotateSlice(tmp2, i*batchSize) + for j := range rot1 { + values[j] = (values[j] + rot1[j]) % tc.Params.PlaintextModulus() + values[j+halfN] = (values[j+halfN] + rot2[j]) % tc.Params.PlaintextModulus() + } + } + if aggregate { + for i := range tmp1 { + values[i] = (values[i] + values[i+halfN]) % tc.Params.PlaintextModulus() } } } - for _, N := range []int{tc.Params.N(), tc.Params.MaxSlots()} { - for _, lvl := range testLevel { - t.Run(name("Evaluator/InnerSum/N slots", tc, lvl), func(t *testing.T) { - if lvl == 0 { - t.Skip("Skipping: Level = 0") - } - n := N >> 2 - batchSize := 1 << 2 + for _, i := range []int{0, 1, 2} { + // n*batchSize = N, N/2, N/8 + for _, offset := range []int{0, 1, 3} { + for _, lvl := range testLevel { + t.Run(name("Evaluator/InnerSum/", tc, lvl), func(t *testing.T) { + if lvl == 0 { + t.Skip("Skipping: Level = 0") + } + n := tc.Params.MaxSlots() >> (i + offset) + batchSize := 1 << i - galEls := tc.Params.GaloisElementsForInnerSum(batchSize, n) - evl := tc.Evl.WithKey(rlwe.NewMemEvaluationKeySet(nil, tc.Kgen.GenGaloisKeysNew(galEls, tc.Sk)...)) + galEls := tc.Params.GaloisElementsForInnerSum(batchSize, n) + evl := tc.Evl.WithKey(rlwe.NewMemEvaluationKeySet(nil, tc.Kgen.GenGaloisKeysNew(galEls, tc.Sk)...)) - want, _, ciphertext0 := NewTestVector(tc.Params, tc.Ecd, tc.Enc, lvl, tc.Params.NewScale(3)) + want, _, ciphertext0 := NewTestVector(tc.Params, tc.Ecd, tc.Enc, lvl, tc.Params.NewScale(3)) - innersum(want, n, batchSize) + innersum(want, n, batchSize, false) - receiver := NewCiphertext(tc.Params, 1, lvl) + receiver := NewCiphertext(tc.Params, 1, lvl) - require.NoError(t, evl.InnerSum(ciphertext0, batchSize, n, receiver)) + require.NoError(t, evl.InnerSum(ciphertext0, batchSize, n, receiver)) - have := make([]uint64, len(want)) - require.NoError(t, tc.Ecd.Decode(tc.Dec.DecryptNew(receiver), have)) + have := make([]uint64, len(want)) + require.NoError(t, tc.Ecd.Decode(tc.Dec.DecryptNew(receiver), have)) - for i := 0; i < len(want); i += n * batchSize { - require.Equal(t, want[i:i+batchSize], have[i:i+batchSize]) - } - }) + for i := 0; i < len(want); i += n * batchSize { + require.Equal(t, want[i:i+batchSize], have[i:i+batchSize]) + } + }) + } } - } - for _, lvl := range testLevel { - t.Run(name("Evaluator/InnerSum/N/2 slots", tc, lvl), func(t *testing.T) { - if lvl == 0 { - t.Skip("Skipping: Level = 0") - } - n := 7 - batchSize := 13 - l := n * batchSize - halfN := tc.Params.MaxSlots() >> 1 + // Test RotateAndAdd with n*batchSize dividing and not dividing #slots + for _, n := range []int{tc.Params.MaxSlots() >> 3, 7} { + for _, batchSize := range []int{8, 3} { + for _, lvl := range testLevel { + t.Run(name("Evaluator/RotateAndAdd/", tc, lvl), func(t *testing.T) { + if lvl == 0 { + t.Skip("Skipping: Level = 0") + } - galEls := tc.Params.GaloisElementsForInnerSum(batchSize, n) - evl := tc.Evl.WithKey(rlwe.NewMemEvaluationKeySet(nil, tc.Kgen.GenGaloisKeysNew(galEls, tc.Sk)...)) + galEls := tc.Params.GaloisElementsForInnerSum(batchSize, n) + evl := tc.Evl.WithKey(rlwe.NewMemEvaluationKeySet(nil, tc.Kgen.GenGaloisKeysNew(galEls, tc.Sk)...)) - want, _, ciphertext0 := NewTestVector(tc.Params, tc.Ecd, tc.Enc, lvl, tc.Params.NewScale(3)) + want, _, ciphertext0 := NewTestVector(tc.Params, tc.Ecd, tc.Enc, lvl, tc.Params.NewScale(3)) - innersum(want[:halfN], n, batchSize) - innersum(want[halfN:], n, batchSize) + innersum(want, n, batchSize, true) - receiver := NewCiphertext(tc.Params, 1, lvl) + receiver := NewCiphertext(tc.Params, 1, lvl) - require.NoError(t, evl.InnerSum(ciphertext0, batchSize, n, receiver)) + require.NoError(t, evl.RotateAndAdd(ciphertext0, batchSize, n, receiver)) - have := make([]uint64, len(want)) - require.NoError(t, tc.Ecd.Decode(tc.Dec.DecryptNew(receiver), have)) + have := make([]uint64, len(want)) + require.NoError(t, tc.Ecd.Decode(tc.Dec.DecryptNew(receiver), have)) - for i, j := 0, halfN; i < halfN; i, j = i+l, j+l { - require.Equal(t, want[i:i+batchSize], have[i:i+batchSize]) - require.Equal(t, want[j:j+batchSize], have[j:j+batchSize]) + require.Equal(t, want, have) + }) + } } - }) + } } } diff --git a/schemes/bgv/evaluator.go b/schemes/bgv/evaluator.go index 7b69557a8..279e90784 100644 --- a/schemes/bgv/evaluator.go +++ b/schemes/bgv/evaluator.go @@ -1505,41 +1505,83 @@ func (eval Evaluator) RotateHoistedLazyNew(level int, rotations []int, op0 *rlwe return } -// InnerSum computes the inner sum of the underlying slots (see [rlwe.Evaluator.InnerSum]). -// NB: in the slot encoding of BGV/BFV, the underlying N slots are arranged as 2 rows of N/2 slots. -// If n*batchSize is a multiple of N, InnerSum computes the [rlwe.Evaluator.InnerSum] on the N slots. -// NOTE: In this case, InnerSum performs an addition and a [Evaluator.RotateRowsNew] on top. -// Otherwise, InnerSum computes the [rlwe.Evaluator.InnerSum] of each row separately. +// InnerSum divides each row of the underlying plaintext in sub-vectors of size batchSize and add n of these together. +// If n*batchSize = ctIn.Slots(), the inner sum is computed as if the plaintext was a 1-D vector of dimension ctIn.Slots() +// (we recall that a BGV/BFV plaintext is represented as a 2 x ctIn.Slots()/2 matrix). +// +// WARNING: 0 < n*batchSize <= ctIn.Slots() must divide the number of slots ctIn.Slots(). For other parameters, consider using [Evaluator.RotateAndAdd]. +// +// Example for batchSize=2, n=4 and 32 slots (garbage slots are marked as X): +// +// Input: +// +// [[{a1, b1}, {c1, d1}, {e1, f1}, {g1, h1}, {i1, j1}, {k1, l1}, {m1, n1}, {o1, p1}] +// +// [{a2, b2}, {c2, d2}, {e2, f2}, {g2, h2}, {i2, j2}, {k2, l2}, {m2, n2}, {o2, p2}]] +// +// Output: +// +// [[{a1+c1+e1+g1, b1+d1+f1+h1}, {X, X}, {X, X}, {X, X}, {i1+k1+m1+o1, j1+l1+n1+p1}, {X, X}, {X, X}, {X, X}] +// +// [{a2+c2+e2+g2, b2+d2+f2+h2}, {X, X}, {X, X}, {X, X}, {i2+k2+m2+o2, j2+l2+n2+p2}, {X, X}, {X, X}, {X, X}]] func (eval Evaluator) InnerSum(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *rlwe.Ciphertext) (err error) { - N := eval.parameters.MaxSlots() + N := ctIn.Slots() l := n * batchSize - if l%N == 0 { + if n <= 0 || batchSize <= 0 { + return fmt.Errorf("innersum: invalid parameter (n <= 0 or batchSize <= 0)") + } + if l > N { + return fmt.Errorf("innersum: invalid parameters (n*batchSize=%d > #slots=%d)", l, N) + } + if l&(l-1) != 0 { + return fmt.Errorf("innersum: invalid parameters (n*batchSize=%d does not divide #slots=%d)", l, N) + } + + if l == N { if n == 1 { - if ctIn != opOut { - opOut.Copy(ctIn) - } + opOut.Copy(ctIn) return } - if err = eval.Evaluator.InnerSum(ctIn, batchSize, n/2, opOut); err != nil { + if err = eval.Evaluator.PartialTrace(ctIn, batchSize, n/2, opOut); err != nil { return } - var ctRot *rlwe.Ciphertext - ctRot, err = eval.RotateRowsNew(opOut) - if err != nil { + ctTmp := &rlwe.Ciphertext{Element: rlwe.Element[ring.Poly]{Value: []ring.Poly{eval.BuffQP[2].Q, eval.BuffQP[3].Q}}} + ctTmp.MetaData = opOut.MetaData + if err = eval.RotateRows(opOut, ctTmp); err != nil { return } - if err = eval.Add(opOut, ctRot, opOut); err != nil { + if err = eval.Add(opOut, ctTmp, opOut); err != nil { return } return } - err = eval.Evaluator.InnerSum(ctIn, batchSize, n, opOut) + err = eval.Evaluator.PartialTrace(ctIn, batchSize, n, opOut) + return +} + +// RotateAndAdd computes the sum of pt_i, 0 <= i < n, where pt_i is the underlying plaintext rotated ([Evaluator.RotateRows]) by batchSize*i slots. +// +// Example: for batchSize=3, n=2, ctIn.Slots()=16: +// +// Input (recall that a BGV/BFV plaintext is represented as a 2 x ctIn.Slots()/2 matrix): +// +// [[a, b, c, d, e, f, g, h] +// [i, j, k, l, m, n, o, p]] +// +// Output: +// +// [[a, b, c, d, e, f, g, h] + [[d, e, f, g, h, a, b, c] = [[a+d, b+e, c+f, d+g, e+h, f+a, g+b, h+c] +// [i, j, k, l, m, n, o, p]] [l, m, n, o, p, i, j, k]] [i+l, j+m, k+n, l+o, m+p, n+i, o+j, p+k]] +// +// Calling RotateAndAdd(ctIn, 1, n, opOut) can be used to compute the inner sum of the first n slots of a plaintext. +func (eval Evaluator) RotateAndAdd(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *rlwe.Ciphertext) (err error) { + err = eval.Evaluator.PartialTrace(ctIn, batchSize, n, opOut) return } diff --git a/schemes/bgv/params.go b/schemes/bgv/params.go index 84d405096..6b24b19bb 100644 --- a/schemes/bgv/params.go +++ b/schemes/bgv/params.go @@ -257,7 +257,7 @@ func (p Parameters) GaloisElementForRowRotation() uint64 { // InnerSum operation with parameters batch and n. func (p Parameters) GaloisElementsForInnerSum(batch, n int) (galEls []uint64) { galEls = rlwe.GaloisElementsForInnerSum(p, batch, n) - if n*batch%p.MaxSlots() == 0 { + if n*batch > p.MaxSlots()>>1 { galEls = append(galEls, p.GaloisElementForRowRotation()) } return diff --git a/schemes/ckks/evaluator.go b/schemes/ckks/evaluator.go index d64f2524d..07050b29e 100644 --- a/schemes/ckks/evaluator.go +++ b/schemes/ckks/evaluator.go @@ -1268,6 +1268,54 @@ func (eval Evaluator) RotateHoistedLazyNew(level int, rotations []int, ct *rlwe. return } +// InnerSum divides each row of the underlying plaintext in sub-vectors of size batchSize and add n of these together. +// +// WARNING: 0 < n*batchSize <= ctIn.Slots() must divide the number of slots ctIn.Slots(). For other parameters, consider using [Evaluator.RotateAndAdd]. +// +// Example for batchSize=2, n=4 and 16 slots (garbage slots are marked as X): +// +// Input: +// +// [{a, b}, {c, d}, {e, f}, {g, h}, {i, j}, {k, l}, {m, n}, {o, p}] +// +// Output: +// +// [{a+c+e+g, b+d+f+h}, {X, X}, {X, X}, {X, X}, {i+k+m+o, j+l+n+p}, {X, X}, {X, X}, {X, X}] +func (eval Evaluator) InnerSum(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *rlwe.Ciphertext) (err error) { + N := ctIn.Slots() + l := n * batchSize + + if n <= 0 || batchSize <= 0 { + return fmt.Errorf("innersum: invalid parameter (n <= 0 or batchSize <= 0)") + } + if l > N { + return fmt.Errorf("innersum: invalid parameters (n*batchSize=%d > #slots=%d)", l, N) + } + if l&(l-1) != 0 { + return fmt.Errorf("innersum: invalid parameters (n*batchSize=%d does not divide #slots=%d)", l, N) + } + err = eval.Evaluator.PartialTrace(ctIn, batchSize, n, opOut) + return +} + +// RotateAndAdd computes the sum of pt_i, 0 <= i < n, where pt_i is the underlying plaintext rotated ([Evaluator.Rotate]) by batchSize*i slots. +// +// Example: for batchSize=3, n=2, ctIn.Slots()=8: +// +// Input: +// +// [a, b, c, d, e, f, g, h] +// +// Output: +// +// [a, b, c, d, e, f, g, h] + [d, e, f, g, h, a, b, c] = [a+d, b+e, c+f, d+g, e+h, f+a, g+b, h+c] +// +// Calling RotateAndAdd(ctIn, 1, n, opOut) can be used to compute the inner sum of the first n slots of a plaintext. +func (eval Evaluator) RotateAndAdd(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *rlwe.Ciphertext) (err error) { + err = eval.Evaluator.PartialTrace(ctIn, batchSize, n, opOut) + return +} + // ShallowCopy creates a shallow copy of this evaluator in which all the read-only data-structures are // shared with the receiver and the temporary buffers are reallocated. The receiver and the returned // Evaluators can be used concurrently. From af750eff230717c5df9e9981bcd48eab3ed73d95 Mon Sep 17 00:00:00 2001 From: lehugueni Date: Tue, 17 Dec 2024 16:43:07 +0100 Subject: [PATCH 5/6] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1327de57b..a2ff8773b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this library are documented in this file. ## [6.x.x] - 16.12.2024 - Refactoring of the InnerSum methods: - `rlwe.Evaluator.InnerSum` has been replaced by `rlwe.Evaluator.PartialTrace` - - Introduction of the `bgv.Evaluator.InnerSum` and `ckks.Evaluator.InnerSum` methods, which have the same behaviour as the old `InnerSum` method for parameters `n` and `batchSize` s.t. `n*batchSize` divides the number of slots. Parameters not satisfying this condition are rejected. + - Introduction of the `bgv.Evaluator.InnerSum` and `ckks.Evaluator.InnerSum` methods, which have the same behaviour as the old `InnerSum` method for parameters `n` and `batchSize` s.t. `0 < n*batchSize <= ctIn.Slots()` divides the number of slots. Parameters not satisfying these conditions are rejected. - Introduction of the `bgv.Evaluator.RotateAndAdd` and `ckks.Evaluator.RotateAndAdd` methods, which have the same behaviour as the old `InnerSum` method for all parameters. ## [6.1.0] - 04.10.2024 From dda9b252213097430e9a8711ed9f79997131e7bc Mon Sep 17 00:00:00 2001 From: lehugueni Date: Wed, 18 Dec 2024 15:24:40 +0100 Subject: [PATCH 6/6] changelog + change name partialtrace --- CHANGELOG.md | 2 +- core/rlwe/inner_sum.go | 7 ++++--- core/rlwe/rlwe_test.go | 2 +- schemes/bgv/evaluator.go | 6 +++--- schemes/ckks/evaluator.go | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2ff8773b..3ca42c6d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this library are documented in this file. ## [6.x.x] - 16.12.2024 - Refactoring of the InnerSum methods: - - `rlwe.Evaluator.InnerSum` has been replaced by `rlwe.Evaluator.PartialTrace` + - `rlwe.Evaluator.InnerSum` has been replaced by `rlwe.Evaluator.PartialTracesSum`, which applies the automorphisms that correspond to rotations at the scheme level (and sum the results). - Introduction of the `bgv.Evaluator.InnerSum` and `ckks.Evaluator.InnerSum` methods, which have the same behaviour as the old `InnerSum` method for parameters `n` and `batchSize` s.t. `0 < n*batchSize <= ctIn.Slots()` divides the number of slots. Parameters not satisfying these conditions are rejected. - Introduction of the `bgv.Evaluator.RotateAndAdd` and `ckks.Evaluator.RotateAndAdd` methods, which have the same behaviour as the old `InnerSum` method for all parameters. diff --git a/core/rlwe/inner_sum.go b/core/rlwe/inner_sum.go index 39c395b10..d41fd462d 100644 --- a/core/rlwe/inner_sum.go +++ b/core/rlwe/inner_sum.go @@ -144,10 +144,11 @@ func GaloisElementsForTrace(params ParameterProvider, logN int) (galEls []uint64 return } -// PartialTrace applies a partial trace on the input ciphertext with the automorphisms phi(i*offset, X), 0 <= i < n, where phi(k, X): X -> X^{5^k} +// PartialTracesSum applies a set of automorphisms on the input ciphertext and sum the results. +// The automorphisms are of the form phi(i*offset, X), 0 <= i < n, where phi(k, X): X -> X^{5^k} // i.e. opOut = \sum_{i = 0}^{n-1} phi(i*offset, ctIn). // At the scheme level, this function is used to perform inner sums or efficiently replicate slots. -func (eval Evaluator) PartialTrace(ctIn *Ciphertext, offset, n int, opOut *Ciphertext) (err error) { +func (eval Evaluator) PartialTracesSum(ctIn *Ciphertext, offset, n int, opOut *Ciphertext) (err error) { if n == 0 || offset == 0 { return fmt.Errorf("partialtrace: invalid parameter (n = 0 or batchSize = 0)") } @@ -473,7 +474,7 @@ func GaloisElementsForInnerSum(params ParameterProvider, batch, n int) (galEls [ // two consecutive sub-vectors to replicate. // This method is faster than Replicate when the number of rotations is large and it uses log2(n) + HW(n) instead of n. func (eval Evaluator) Replicate(ctIn *Ciphertext, batchSize, n int, opOut *Ciphertext) (err error) { - return eval.PartialTrace(ctIn, -batchSize, n, opOut) + return eval.PartialTracesSum(ctIn, -batchSize, n, opOut) } // GaloisElementsForReplicate returns the list of Galois elements necessary to perform the diff --git a/core/rlwe/rlwe_test.go b/core/rlwe/rlwe_test.go index da09828b4..d7b0c75af 100644 --- a/core/rlwe/rlwe_test.go +++ b/core/rlwe/rlwe_test.go @@ -1103,7 +1103,7 @@ func testSlotOperations(tc *TestContext, level, bpw2 int, t *testing.T) { // Galois Keys evk := NewMemEvaluationKeySet(nil, kgen.GenGaloisKeysNew(GaloisElementsForInnerSum(params, batch, n), sk)...) - require.NoError(t, eval.WithKey(evk).PartialTrace(ct, batch, n, ct)) + require.NoError(t, eval.WithKey(evk).PartialTracesSum(ct, batch, n, ct)) dec.Decrypt(ct, pt) diff --git a/schemes/bgv/evaluator.go b/schemes/bgv/evaluator.go index 279e90784..50124e548 100644 --- a/schemes/bgv/evaluator.go +++ b/schemes/bgv/evaluator.go @@ -1544,7 +1544,7 @@ func (eval Evaluator) InnerSum(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *r return } - if err = eval.Evaluator.PartialTrace(ctIn, batchSize, n/2, opOut); err != nil { + if err = eval.Evaluator.PartialTracesSum(ctIn, batchSize, n/2, opOut); err != nil { return } @@ -1561,7 +1561,7 @@ func (eval Evaluator) InnerSum(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *r return } - err = eval.Evaluator.PartialTrace(ctIn, batchSize, n, opOut) + err = eval.Evaluator.PartialTracesSum(ctIn, batchSize, n, opOut) return } @@ -1581,7 +1581,7 @@ func (eval Evaluator) InnerSum(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *r // // Calling RotateAndAdd(ctIn, 1, n, opOut) can be used to compute the inner sum of the first n slots of a plaintext. func (eval Evaluator) RotateAndAdd(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *rlwe.Ciphertext) (err error) { - err = eval.Evaluator.PartialTrace(ctIn, batchSize, n, opOut) + err = eval.Evaluator.PartialTracesSum(ctIn, batchSize, n, opOut) return } diff --git a/schemes/ckks/evaluator.go b/schemes/ckks/evaluator.go index 07050b29e..7cab4e1b1 100644 --- a/schemes/ckks/evaluator.go +++ b/schemes/ckks/evaluator.go @@ -1294,7 +1294,7 @@ func (eval Evaluator) InnerSum(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *r if l&(l-1) != 0 { return fmt.Errorf("innersum: invalid parameters (n*batchSize=%d does not divide #slots=%d)", l, N) } - err = eval.Evaluator.PartialTrace(ctIn, batchSize, n, opOut) + err = eval.Evaluator.PartialTracesSum(ctIn, batchSize, n, opOut) return } @@ -1312,7 +1312,7 @@ func (eval Evaluator) InnerSum(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *r // // Calling RotateAndAdd(ctIn, 1, n, opOut) can be used to compute the inner sum of the first n slots of a plaintext. func (eval Evaluator) RotateAndAdd(ctIn *rlwe.Ciphertext, batchSize, n int, opOut *rlwe.Ciphertext) (err error) { - err = eval.Evaluator.PartialTrace(ctIn, batchSize, n, opOut) + err = eval.Evaluator.PartialTracesSum(ctIn, batchSize, n, opOut) return }