diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ebd3621..3ca42c6d 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.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. + ## [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 2e588b25..d41fd462 100644 --- a/core/rlwe/inner_sum.go +++ b/core/rlwe/inner_sum.go @@ -144,26 +144,14 @@ 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) { +// 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) 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)") + } params := eval.GetRLWEParameters() @@ -236,7 +224,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 +269,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 +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.InnerSum(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/keygenerator.go b/core/rlwe/keygenerator.go index 9fc49c02..0450f87d 100644 --- a/core/rlwe/keygenerator.go +++ b/core/rlwe/keygenerator.go @@ -279,12 +279,13 @@ func (kgen KeyGenerator) genEvaluationKey(skIn ring.Poly, skOut ringqp.Poly, evk // For a compressed evaluation key, a seed is created and stored in the EvaluationKey struct // struct while an uncompressed key uses an ephemeral seed. if evk.IsCompressed() { - evk.Seed = make([]byte, 32) - if n, err := kgen.prng.Read(evk.Seed); n != 32 || err != nil { + var seed [32]byte + if n, err := kgen.prng.Read(seed[:]); n != 32 || err != nil { panic(fmt.Errorf("unable to sample evaluation key seed")) } + evk.Seed = &seed - sampler, err := sampling.NewKeyedPRNG(evk.Seed) + sampler, err := sampling.NewKeyedPRNG(seed[:]) if err != nil { panic(fmt.Errorf("sampling.NewKeyedPRNG: %w", err)) } diff --git a/core/rlwe/keys.go b/core/rlwe/keys.go index 35585d52..ac1f2c6f 100644 --- a/core/rlwe/keys.go +++ b/core/rlwe/keys.go @@ -292,7 +292,7 @@ func (p *PublicKey) isEncryptionKey() {} // is used to bring it back to its original key. type EvaluationKey struct { GadgetCiphertext - Seed []byte + Seed *[32]byte // Must be != nil iff EvaluationKey.IsCompressed() = true } type EvaluationKeyParameters struct { @@ -362,7 +362,11 @@ func (evk EvaluationKey) Expand(params ParameterProvider, buffer *GadgetCipherte return fmt.Errorf("evaluation key is not compressed") } - prng, err := sampling.NewKeyedPRNG(evk.Seed) + if evk.Seed == nil { + return fmt.Errorf("seed is missing") + } + + prng, err := sampling.NewKeyedPRNG((*evk.Seed)[:]) if err != nil { panic(fmt.Errorf("sampling.NewKeyedPRNG: %s", err)) } @@ -417,6 +421,115 @@ func (evk EvaluationKey) Expand(params ParameterProvider, buffer *GadgetCipherte return nil } +// BinarySize returns the serialized size of the object in bytes. +func (evk EvaluationKey) BinarySize() (size int) { + if evk.Seed != nil { + return evk.GadgetCiphertext.BinarySize() + len(*evk.Seed) + } + return evk.GadgetCiphertext.BinarySize() +} + +// WriteTo writes the object on an [io.Writer]. It implements the [io.WriterTo] +// interface, and will write exactly object.BinarySize() bytes on w. +// +// Unless w implements the [buffer.Writer] interface (see lattigo/utils/buffer/writer.go), +// it will be wrapped into a [bufio.Writer]. Since this requires allocations, it +// is preferable to pass a [buffer.Writer] directly: +// +// - When writing multiple times to a [io.Writer], it is preferable to first wrap the +// io.Writer in a pre-allocated [bufio.Writer]. +// - When writing to a pre-allocated var b []byte, it is preferable to pass +// buffer.NewBuffer(b) as w (see lattigo/utils/buffer/buffer.go). +func (evk EvaluationKey) WriteTo(w io.Writer) (n int64, err error) { + switch w := w.(type) { + case buffer.Writer: + + var inc int64 + + if inc, err = evk.GadgetCiphertext.WriteTo(w); err != nil { + return n + inc, err + } + + n += inc + + if evk.IsCompressed() { + + // Sanity check, should not happen unless evk has been manually modified + if evk.Seed == nil { + return n + inc, fmt.Errorf("writing compressed evaluation key: the seed is nil") + } + + if inc, err = buffer.Write(w, (*evk.Seed)[:]); err != nil { + return n + inc, err + } + + n += inc + } + + if err = w.Flush(); err != nil { + return n, err + } + return + + default: + return evk.WriteTo(bufio.NewWriter(w)) + } +} + +// ReadFrom reads on the object from an [io.Writer]. It implements the +// [io.ReaderFrom] interface. +// +// Unless r implements the [buffer.Reader] interface (see see lattigo/utils/buffer/reader.go), +// it will be wrapped into a [bufio.Reader]. Since this requires allocation, it +// is preferable to pass a [buffer.Reader] directly: +// +// - When reading multiple values from a [io.Reader], it is preferable to first +// first wrap io.Reader in a pre-allocated [bufio.Reader]. +// - When reading from a var b []byte, it is preferable to pass a buffer.NewBuffer(b) +// as w (see lattigo/utils/buffer/buffer.go). +func (evk *EvaluationKey) ReadFrom(r io.Reader) (n int64, err error) { + switch r := r.(type) { + case buffer.Reader: + + var inc int64 + + if inc, err = evk.GadgetCiphertext.ReadFrom(r); err != nil { + return n + inc, err + } + + n += inc + + if evk.IsCompressed() { + var seed [32]byte + if inc, err = buffer.Read(r, seed[:]); err != nil { + return n + inc, err + } + + evk.Seed = &seed + + n += inc + } + + return + default: + return evk.ReadFrom(bufio.NewReader(r)) + } +} + +// MarshalBinary encodes the object into a binary form on a newly allocated slice of bytes. +func (evk EvaluationKey) MarshalBinary() (p []byte, err error) { + buf := buffer.NewBufferSize(evk.BinarySize()) + _, err = evk.WriteTo(buf) + return buf.Bytes(), err +} + +// UnmarshalBinary decodes a slice of bytes generated by +// [EvaluationKey.MarshalBinary] or [EvaluationKey.WriteTo] on the object. +func (evk *EvaluationKey) UnmarshalBinary(p []byte) (err error) { + _, err = evk.ReadFrom(buffer.NewBuffer(p)) + return +} + // CopyNew creates a deep copy of the target [EvaluationKey] and returns it. func (evk EvaluationKey) CopyNew() *EvaluationKey { return &EvaluationKey{GadgetCiphertext: *evk.GadgetCiphertext.CopyNew()} diff --git a/core/rlwe/rlwe_test.go b/core/rlwe/rlwe_test.go index f76dddf3..5ddcd1be 100644 --- a/core/rlwe/rlwe_test.go +++ b/core/rlwe/rlwe_test.go @@ -41,7 +41,7 @@ func testString(params Parameters, levelQ, levelP, bpw2 int, opname string) stri func TestRLWEConstSerialization(t *testing.T) { // Note: changing nbIteration will change the expected value const nbIteration = 10 - const expected = "XRdlwx5vEX9qdGY3CeeAxzGHa0gbXghzpLhV0eIgVk8=" + const expected = "/mTt2kB+03NdOMoI1msW+glCZmrF1sxEGQkFsC6P1SA=" var err error defaultParamsLiteral := testInsecure seedKeyGen := []byte{'l', 'a', 't'} @@ -77,16 +77,20 @@ func TestRLWEConstSerialization(t *testing.T) { hash.Write(pkBytes) // Add marshalled GaloisKey to the hash input - galEl := params.GaloisElement(-1) + galEl1 := params.GaloisElement(-1) galEl2 := params.GaloisElement(3) - galKey := detTC.kgen.GenGaloisKeysNew([]uint64{galEl, galEl2}, sk) - galKeyBytes, err := galKey[0].MarshalBinary() + galKey1 := detTC.kgen.GenGaloisKeyNew(galEl1, sk) + galKey2 := detTC.kgen.GenGaloisKeyNew(galEl2, sk, EvaluationKeyParameters{Compressed: true}) + galKeyBytes, err := galKey1.MarshalBinary() + require.Nil(t, err) + hash.Write(galKeyBytes) + galKeyBytes, err = galKey2.MarshalBinary() require.Nil(t, err) hash.Write(galKeyBytes) // Add marshalled MemEvaluationKeySet to the hash input relinKey := detTC.kgen.GenRelinearizationKeyNew(sk) - evk := NewMemEvaluationKeySet(relinKey, galKey...) + evk := NewMemEvaluationKeySet(relinKey, galKey1, galKey2) evkBytes, err := evk.MarshalBinary() require.Nil(t, err) hash.Write(evkBytes) @@ -1083,7 +1087,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 +1099,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 +1107,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).PartialTracesSum(ct, batch, n, ct)) dec.Decrypt(ct, pt) @@ -1238,6 +1243,10 @@ func testWriteAndRead(tc *TestContext, bpw2 int, t *testing.T) { buffer.RequireSerializerCorrect(t, tc.kgen.GenEvaluationKeyNew(sk, sk)) }) + t.Run(testString(params, levelQ, levelP, bpw2, "WriteAndRead/EvaluationKey/Compressed=True"), func(t *testing.T) { + buffer.RequireSerializerCorrect(t, tc.kgen.GenEvaluationKeyNew(sk, sk, EvaluationKeyParameters{Compressed: true})) + }) + t.Run(testString(params, levelQ, levelP, bpw2, "WriteAndRead/RelinearizationKey"), func(t *testing.T) { buffer.RequireSerializerCorrect(t, tc.kgen.GenRelinearizationKeyNew(tc.sk)) }) diff --git a/examples/singleparty/tutorials/ckks/main.go b/examples/singleparty/tutorials/ckks/main.go index 40b44159..b24937a4 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 0dca91dd..26768979 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,95 @@ func testEvaluatorBvg(tc *TestContext, t *testing.T) { } }) } + + // Naive implementation of the inner sum for reference + 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++ { + 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 _, 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)...)) + + want, _, ciphertext0 := NewTestVector(tc.Params, tc.Ecd, tc.Enc, lvl, tc.Params.NewScale(3)) + + innersum(want, n, batchSize, false) + + 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]) + } + }) + } + } + + // 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)...)) + + want, _, ciphertext0 := NewTestVector(tc.Params, tc.Ecd, tc.Enc, lvl, tc.Params.NewScale(3)) + + innersum(want, n, batchSize, true) + + receiver := NewCiphertext(tc.Params, 1, lvl) + + 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)) + + require.Equal(t, want, have) + }) + } + } + } + } } func testEvaluatorBfv(tc *TestContext, t *testing.T) { diff --git a/schemes/bgv/evaluator.go b/schemes/bgv/evaluator.go index 554f7c5f..50124e54 100644 --- a/schemes/bgv/evaluator.go +++ b/schemes/bgv/evaluator.go @@ -1505,6 +1505,86 @@ func (eval Evaluator) RotateHoistedLazyNew(level int, rotations []int, op0 *rlwe return } +// 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 := 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) + } + + if l == N { + if n == 1 { + opOut.Copy(ctIn) + return + } + + if err = eval.Evaluator.PartialTracesSum(ctIn, batchSize, n/2, opOut); err != nil { + return + } + + 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, ctTmp, opOut); err != nil { + return + } + + return + } + + err = eval.Evaluator.PartialTracesSum(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.PartialTracesSum(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 06626383..6b24b19b 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.MaxSlots()>>1 { galEls = append(galEls, p.GaloisElementForRowRotation()) } return diff --git a/schemes/ckks/evaluator.go b/schemes/ckks/evaluator.go index d64f2524..7cab4e1b 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.PartialTracesSum(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.PartialTracesSum(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.