HEBench
Quickstart C++ Wrapper Backend Tutorial - SEAL

Backend Creation Tutorial

This tutorial details the steps involved in implementing a new backend utilizing the C++ wrapper for use with the testing framework.

Refer to Glossary for a list of terms that will be mentioned throughout this tutorial.

This guide is intended to be a beginners guide for people who are looking to get a backend with a single test implemented quickly. It is based on using the api_bridge_example_backend as a starting point for a new backend implementation which uses the C++ wrapper.

For the purposes of this guide we will be creating a very basic backend implementing a benchmark for the vector element-wise addition workload, under the offline category, with first operand in plain text and second encrypted, using Microsoft SEAL 3.6 to perform Homomorphic Encryption (HE) operations.

Assume that we already have a functional workflow for our workload as listed below, and we want to benchmark it using HEBench.

8 #include <cassert>
9 #include <cstdint>
10 #include <iostream>
11 #include <memory>
12 #include <random>
13 #include <stdexcept>
14 
15 #include "seal/seal.h"
16 
17 class Workload
18 {
19 public:
20  Workload(std::size_t vector_size);
21 
22  std::vector<seal::Plaintext> encodeVector(const std::vector<std::vector<std::int64_t>> &vec);
23  std::vector<seal::Ciphertext> encryptVector(const std::vector<seal::Plaintext> &encoded_vec);
24  std::vector<seal::Ciphertext> eltwiseadd(const std::vector<seal::Plaintext> &A,
25  const std::vector<seal::Ciphertext> &B);
26  std::vector<seal::Plaintext> decryptResult(const std::vector<seal::Ciphertext> &encrypted_result);
27  std::vector<std::vector<int64_t>> decodeResult(const std::vector<seal::Plaintext> &encoded_result);
28 
29 private:
30  class SealBFVContext
31  {
32  public:
33  SealBFVContext(int poly_modulus_degree)
34  {
35  seal::EncryptionParameters parms{ seal::scheme_type::bfv };
36  parms.set_poly_modulus_degree(poly_modulus_degree);
37  parms.set_coeff_modulus(seal::CoeffModulus::Create(poly_modulus_degree, { 60, 60 }));
38  parms.set_plain_modulus(seal::PlainModulus::Batching(poly_modulus_degree, 20));
39  m_p_seal_context.reset(
40  new seal::SEALContext(parms, true, seal::sec_level_type::tc128));
41  m_p_keygen = std::make_unique<seal::KeyGenerator>(context());
42 
43  m_p_keygen->create_public_key(m_public_key);
44  m_secret_key = m_p_keygen->secret_key();
45 
46  m_p_encryptor = std::make_unique<seal::Encryptor>(context(), m_public_key);
47  m_p_evaluator = std::make_unique<seal::Evaluator>(context());
48  m_p_decryptor = std::make_unique<seal::Decryptor>(context(), m_secret_key);
49  m_p_batch_encoder = std::make_unique<seal::BatchEncoder>(context());
50  }
51 
52  seal::BatchEncoder &encoder() { return *m_p_batch_encoder; }
53  seal::Evaluator &evaluator() { return *m_p_evaluator; }
54  seal::Decryptor &decryptor() { return *m_p_decryptor; }
55  const seal::Encryptor &encryptor() const { return *m_p_encryptor; }
56  const seal::PublicKey &public_key() const { return m_public_key; }
57  const seal::SecretKey &secret_key() const { return m_secret_key; }
58  seal::SEALContext &context() { return *m_p_seal_context; }
59 
60  private:
61  std::shared_ptr<seal::SEALContext> m_p_seal_context;
62  std::unique_ptr<seal::KeyGenerator> m_p_keygen;
63  seal::PublicKey m_public_key;
64  seal::SecretKey m_secret_key;
65  std::unique_ptr<seal::Encryptor> m_p_encryptor;
66  std::unique_ptr<seal::Evaluator> m_p_evaluator;
67  std::unique_ptr<seal::Decryptor> m_p_decryptor;
68  std::unique_ptr<seal::BatchEncoder> m_p_batch_encoder;
69  };
70 
71  std::size_t m_vector_size;
72  std::shared_ptr<SealBFVContext> m_p_context;
73 
74  SealBFVContext &context() { return *m_p_context; }
75 };
76 
77 Workload::Workload(std::size_t vector_size)
78 {
79  if (vector_size <= 0)
80  throw std::invalid_argument("vector_size");
81  m_vector_size = vector_size;
82  m_p_context = std::make_shared<SealBFVContext>(8192);
83 }
84 
85 std::vector<seal::Plaintext> Workload::encodeVector(const std::vector<std::vector<std::int64_t>> &vec)
86 {
87  std::vector<seal::Plaintext> retval(vec.size());
88 
89  for (std::size_t i = 0; i < vec.size(); ++i)
90  {
91  assert(vec[i].size() <= context().encoder().slot_count());
92  context().encoder().encode(vec[i], retval[i]);
93  }
94  return retval;
95 }
96 
97 std::vector<seal::Ciphertext> Workload::encryptVector(const std::vector<seal::Plaintext> &encoded_vec)
98 {
99  std::vector<seal::Ciphertext> retval(encoded_vec.size());
100  for (std::size_t i = 0; i < encoded_vec.size(); i++)
101  context().encryptor().encrypt(encoded_vec[i], retval[i]);
102  return retval;
103 }
104 
105 std::vector<seal::Ciphertext> Workload::eltwiseadd(const std::vector<seal::Plaintext> &A,
106  const std::vector<seal::Ciphertext> &B)
107 {
108  std::vector<seal::Ciphertext> retval(A.size() * B.size());
109 
110  // This is the main operation function:
111  // for an offline test, it must store the result of the operation on every
112  // set of input sample in the same order as the input set.
113  // See documentation on "Ordering of Results Based on Input Batch Sizes"
114  // for details.
115  for (std::size_t A_i = 0; A_i < A.size(); ++A_i)
116  for (std::size_t B_i = 0; B_i < B.size(); ++B_i)
117  {
118  seal::Ciphertext &retval_item = retval[A_i * B.size() + B_i];
119  context().evaluator().add_plain(B[B_i], A[A_i], retval_item);
120  }
121 
122  return retval;
123 }
124 
125 std::vector<seal::Plaintext> Workload::decryptResult(const std::vector<seal::Ciphertext> &encrypted_result)
126 {
127  std::vector<seal::Plaintext> retval(encrypted_result.size());
128  for (std::size_t i = 0; i < encrypted_result.size(); i++)
129  context().decryptor().decrypt(encrypted_result[i], retval[i]);
130  return retval;
131 }
132 
133 std::vector<std::vector<int64_t>> Workload::decodeResult(const std::vector<seal::Plaintext> &encoded_result)
134 {
135  std::vector<std::vector<int64_t>> retval(encoded_result.size());
136  for (std::size_t i = 0; i < encoded_result.size(); ++i)
137  {
138  context().encoder().decode(encoded_result[i], retval[i]);
139  retval[i].resize(m_vector_size);
140  }
141  return retval;
142 }
143 
144 //---------------------------------------------------------------------
145 // main() implementation tests the workflow
146 
147 int main()
148 {
149  static const std::size_t VectorSize = 400;
150  static const std::size_t DimensionA = 2;
151  static const std::size_t DimensionB = 5;
152 
153  // START data prep
154  std::vector<std::vector<std::int64_t>> A(DimensionA, std::vector<std::int64_t>(VectorSize));
155  std::vector<std::vector<std::int64_t>> B(DimensionB, std::vector<std::int64_t>(VectorSize));
156 
157  // generate and fill vectors with random data
158  std::random_device rd;
159  std::mt19937 gen(rd());
160  std::uniform_int_distribution<> distrib(-100, 100);
161 
162  for (std::size_t i = 0; i < A.size(); ++i)
163  for (std::size_t j = 0; j < A[i].size(); ++j)
164  A[i][j] = distrib(gen);
165  for (std::size_t i = 0; i < B.size(); ++i)
166  for (std::size_t j = 0; j < B[i].size(); ++j)
167  B[i][j] = distrib(gen);
168  // END data prep
169 
170  //---------------------------------------------------------------------
171 
172  // START Backend
173  std::shared_ptr<Workload> p_workload = // initialize
174  std::make_shared<Workload>(VectorSize);
175  Workload &workload = *p_workload;
176  // For illustration purposes only: all functions in the pipeline should
177  // be able to be nested with each other in the proper order.
178  std::vector<std::vector<std::int64_t>> result =
179  workload.decodeResult(
180  workload.decryptResult(
181  workload.eltwiseadd(workload.encodeVector(A),
182  workload.encryptVector(workload.encodeVector(B)))));
183  p_workload.reset(); // terminate
184  // END Backend
185 
186  //---------------------------------------------------------------------
187 
188  // START Validation
189 
190  std::vector<std::vector<std::int64_t>> exp_out(DimensionA * DimensionB, std::vector<std::int64_t>(VectorSize));
191  assert(exp_out.size() == result.size());
192 
193  // compute ground truth
194  std::size_t result_i = 0;
195  for (std::size_t A_i = 0; A_i < A.size(); ++A_i)
196  for (std::size_t B_i = 0; B_i < B.size(); ++B_i)
197  {
198  for (std::size_t i = 0; i < VectorSize; ++i)
199  exp_out[result_i][i] = A[A_i][i] + B[B_i][i];
200  ++result_i;
201  }
202 
203  if (result == exp_out)
204  std::cout << "OK" << std::endl;
205  else
206  std::cout << "Fail" << std::endl;
207  // END Validation
208 
209  return 0;
210 }
211 
Workload
Defines all possible workloads.
Definition: types.h:83
int main(int argc, char **argv)

What we have to do is extract the stages of the testing pipeline from our workflow and integrate them into the C++ wrapper.

Follow the tutorial steps in order to complete it.

Tutorial steps

Tutorial Home
Preparation
Engine Initialization and Benchmark Description
Benchmark Implementation
File References