HEBench
tutorial_eltwiseadd_benchmark_seal.cpp
Go to the documentation of this file.
1 
2 // Copyright (C) 2021 Intel Corporation
3 // SPDX-License-Identifier: Apache-2.0
4 
6 
7 #include <algorithm>
8 #include <array>
9 #include <cassert>
10 #include <cstring>
11 #include <memory>
12 #include <sstream>
13 #include <utility>
14 #include <vector>
15 
17 #include "tutorial_engine_seal.h"
18 #include "tutorial_error_seal.h"
19 
20 //----------------------------------------------
21 // class TutorialEltwiseAddBenchmarkDescription
22 //----------------------------------------------
23 
25 TutorialEltwiseAddBenchmarkDescription::TutorialEltwiseAddBenchmarkDescription()
26 {
28  // initialize the descriptor for this benchmark
29  std::memset(&m_descriptor, 0, sizeof(hebench::APIBridge::BenchmarkDescriptor));
31 
33  m_descriptor.workload = hebench::APIBridge::Workload::EltwiseAdd;
35 
37  m_descriptor.data_type = hebench::APIBridge::DataType::Int64;
39 
41  m_descriptor.category = hebench::APIBridge::Category::Offline;
43 
45  m_descriptor.cat_params.offline.data_count[0] = 2; // 2 data samples for op parameter 0
46  m_descriptor.cat_params.offline.data_count[1] = 5; // 5 data samples for op parameter 1
48 
50  m_descriptor.cipher_param_mask = 1 << 1;
52 
53  //
55  m_descriptor.scheme = HEBENCH_HE_SCHEME_BFV;
57 
59  m_descriptor.security = TUTORIAL_HE_SECURITY_128;
61 
63  m_descriptor.other = 0; // no extras needed for our purpose:
64  // Other backends can use this field to differentiate between
65  // benchmarks for which internal parameters, not specified by
66  // other fields of this structure, differ.
68 
70  // specify default arguments for this workload flexible parameters:
71  hebench::cpp::WorkloadParams::EltwiseAdd default_workload_params;
72  default_workload_params.n() = 400;
73  this->addDefaultParameters(default_workload_params);
75 }
77 
78 TutorialEltwiseAddBenchmarkDescription::~TutorialEltwiseAddBenchmarkDescription()
79 {
80  // nothing needed in this example
81 }
82 
84 std::string TutorialEltwiseAddBenchmarkDescription::getBenchmarkDescription(const hebench::APIBridge::WorkloadParams *p_w_params) const
85 {
86  std::stringstream ss;
87  ss << BenchmarkDescription::getBenchmarkDescription(p_w_params);
88  ss << ", Encryption parameters" << std::endl
89  << ", , HE Library, Microsoft SEAL 3.6" << std::endl
90  << ", , Poly modulus degree, " << PolyModulusDegree << std::endl
91  << ", , Coefficient Moduli, 60, 60";
92  if (p_w_params)
93  {
94  hebench::cpp::WorkloadParams::EltwiseAdd w_params(*p_w_params);
95  ss << std::endl
96  << ", Workload parameters" << std::endl
97  << ", , n, " << w_params.n();
98  } // end if
99  return ss.str();
100 }
102 
105  const hebench::APIBridge::WorkloadParams *p_params)
106 {
107  if (!p_params)
108  throw hebench::cpp::HEBenchError(HEBERROR_MSG_CLASS("Invalid empty workload parameters. This workload requires flexible parameters."),
110 
111  TutorialEngine &ex_engine = dynamic_cast<TutorialEngine &>(engine);
112  return new TutorialEltwiseAddBenchmark(ex_engine, m_descriptor, *p_params);
113 }
115 
117 void TutorialEltwiseAddBenchmarkDescription::destroyBenchmark(hebench::cpp::BaseBenchmark *p_bench)
118 {
119  if (p_bench)
120  delete p_bench;
121 }
123 
124 //---------------------------------------------
125 // class TutorialEltwiseAddBenchmark::Workload
126 //---------------------------------------------
127 
129 TutorialEltwiseAddBenchmark::Workload::SealBFVContext::SealBFVContext(int poly_modulus_degree)
130 {
131  assert(TutorialEltwiseAddBenchmarkDescription::NumCoefficientModuli == 1);
132 
133  seal::EncryptionParameters parms{ seal::scheme_type::bfv };
134  parms.set_poly_modulus_degree(poly_modulus_degree);
135  parms.set_coeff_modulus(seal::CoeffModulus::Create(poly_modulus_degree, { 60, 60 }));
136  parms.set_plain_modulus(seal::PlainModulus::Batching(poly_modulus_degree, 20));
137  m_p_seal_context.reset(new seal::SEALContext(parms, true, seal::sec_level_type::tc128));
138  m_p_keygen = std::make_unique<seal::KeyGenerator>(context());
139 
140  m_p_keygen->create_public_key(m_public_key);
141  m_secret_key = m_p_keygen->secret_key();
142 
143  m_p_encryptor = std::make_unique<seal::Encryptor>(context(), m_public_key);
144  m_p_evaluator = std::make_unique<seal::Evaluator>(context());
145  m_p_decryptor = std::make_unique<seal::Decryptor>(context(), m_secret_key);
146  m_p_batch_encoder = std::make_unique<seal::BatchEncoder>(context());
147 }
148 
150 {
151  if (vector_size <= 0)
152  throw std::invalid_argument("vector_size");
153  m_vector_size = vector_size;
154  m_p_context = std::make_shared<SealBFVContext>(TutorialEltwiseAddBenchmarkDescription::PolyModulusDegree);
155 }
156 
157 std::vector<seal::Plaintext> TutorialEltwiseAddBenchmark::Workload::encodeVector(const std::vector<std::vector<std::int64_t>> &vec)
158 {
159  std::vector<gsl::span<const std::int64_t>> vec_span(vec.begin(), vec.end());
160  return encodeVector(vec_span);
161 }
162 
163 std::vector<seal::Plaintext> TutorialEltwiseAddBenchmark::Workload::encodeVector(const std::vector<gsl::span<const std::int64_t>> &vec)
164 {
165  std::vector<seal::Plaintext> retval(vec.size());
166 
167  for (std::size_t i = 0; i < vec.size(); ++i)
168  {
169  assert(vec[i].size() <= context().encoder().slot_count());
170  context().encoder().encode(vec[i], retval[i]);
171  }
172  return retval;
173 }
174 
175 std::vector<seal::Ciphertext> TutorialEltwiseAddBenchmark::Workload::encryptVector(const std::vector<seal::Plaintext> &encoded_vec)
176 {
177  std::vector<seal::Ciphertext> retval(encoded_vec.size());
178  for (std::size_t i = 0; i < encoded_vec.size(); i++)
179  context().encryptor().encrypt(encoded_vec[i], retval[i]);
180  return retval;
181 }
182 
183 std::vector<seal::Ciphertext> TutorialEltwiseAddBenchmark::Workload::eltwiseadd(const std::vector<seal::Plaintext> &A,
184  const std::vector<seal::Ciphertext> &B)
185 {
186  std::vector<seal::Ciphertext> retval(A.size() * B.size());
187  for (std::size_t A_i = 0; A_i < A.size(); ++A_i)
188  for (std::size_t B_i = 0; B_i < B.size(); ++B_i)
189  {
190  seal::Ciphertext &retval_item = retval[A_i * B.size() + B_i];
191  context().evaluator().add_plain(B[B_i], A[A_i], retval_item);
192  }
193 
194  return retval;
195 }
196 
197 std::vector<seal::Plaintext> TutorialEltwiseAddBenchmark::Workload::decryptResult(const std::vector<seal::Ciphertext> &encrypted_result)
198 {
199  std::vector<seal::Plaintext> retval(encrypted_result.size());
200  for (std::size_t i = 0; i < encrypted_result.size(); i++)
201  context().decryptor().decrypt(encrypted_result[i], retval[i]);
202  return retval;
203 }
204 
205 std::vector<std::vector<int64_t>> TutorialEltwiseAddBenchmark::Workload::decodeResult(const std::vector<seal::Plaintext> &encoded_result)
206 {
207  std::vector<std::vector<int64_t>> retval(encoded_result.size());
208  for (std::size_t i = 0; i < encoded_result.size(); ++i)
209  {
210  context().encoder().decode(encoded_result[i], retval[i]);
211  retval[i].resize(m_vector_size);
212  }
213  return retval;
214 }
216 
217 //-----------------------------------
218 // class TutorialEltwiseAddBenchmark
219 //-----------------------------------
220 
222 TutorialEltwiseAddBenchmark::TutorialEltwiseAddBenchmark(hebench::cpp::BaseEngine &engine,
223  const hebench::APIBridge::BenchmarkDescriptor &bench_desc,
224  const hebench::APIBridge::WorkloadParams &bench_params) :
225  hebench::cpp::BaseBenchmark(engine, bench_desc, bench_params)
226 {
227  // validate workload parameters
228 
229  // number of workload parameters (1 for eltwise add: n)
230  if (bench_params.count < TutorialEltwiseAddBenchmarkDescription::NumWorkloadParams)
231  throw hebench::cpp::HEBenchError(HEBERROR_MSG_CLASS("Invalid workload parameters. This workload requires "
232  + std::to_string(TutorialEltwiseAddBenchmarkDescription::NumWorkloadParams)
233  + "parameters."),
235 
236  // check values of the workload parameters and make sure they are supported by benchmark:
237  hebench::cpp::WorkloadParams::EltwiseAdd w_params(bench_params);
238  if (w_params.n() <= 0
239  || w_params.n() - 1 > TutorialEltwiseAddBenchmarkDescription::PolyModulusDegree / 2)
240  throw hebench::cpp::HEBenchError(HEBERROR_MSG_CLASS("Invalid workload parameters. This workload only supports vectors of size up to "
241  + std::to_string(TutorialEltwiseAddBenchmarkDescription::PolyModulusDegree / 2)),
243 
244  // Do any extra workload-parameter-based initialization here, if needed.
245 
246  // initialize original Workload (this initializes SEAL BFV context)
247  m_p_workload = std::make_shared<Workload>(w_params.n());
248 }
250 
252 TutorialEltwiseAddBenchmark::~TutorialEltwiseAddBenchmark()
253 {
254  m_p_workload.reset(); // added for clarity
255 }
257 
260 {
261  assert(p_parameters && p_parameters->pack_count > 0 && p_parameters->p_data_packs);
262 
263  assert(p_parameters->pack_count == 1);
264 
266  const hebench::APIBridge::DataPack &param_pack = p_parameters->p_data_packs[0];
267 
268  if (param_pack.buffer_count != this->getDescriptor().cat_params.offline.data_count[param_pack.param_position])
269  {
270  std::stringstream ss;
271  ss << "Unexpected number of input samples for operation parameter " << param_pack.param_position
272  << ". Expected " << this->getDescriptor().cat_params.offline.data_count[param_pack.param_position]
273  << ", but " << std::to_string(param_pack.buffer_count) << " received.";
275  }
277 
279  hebench::cpp::WorkloadParams::EltwiseAdd w_params(this->getWorkloadParameters());
280  std::vector<gsl::span<const std::int64_t>> clear_param(param_pack.buffer_count);
281  for (std::size_t sample_i = 0; sample_i < clear_param.size(); ++sample_i)
282  {
283  const hebench::APIBridge::NativeDataBuffer &native_sample = param_pack.p_buffers[sample_i];
284  clear_param[sample_i] =
285  gsl::span<const std::int64_t>(reinterpret_cast<const std::int64_t *>(native_sample.p),
286  native_sample.size / sizeof(std::int64_t));
287  assert(clear_param[sample_i].size() == w_params.n());
288  }
290 
292  std::vector<seal::Plaintext> encoded = m_p_workload->encodeVector(clear_param);
295  InternalParam<seal::Plaintext> retval;
296  retval.samples = std::move(encoded);
297  retval.param_position = param_pack.param_position;
298  retval.tag = InternalParamInfo::tagPlaintext;
299 
300  return this->getEngine().createHandle<decltype(retval)>(
301  sizeof(seal::Plaintext) * retval.samples.size(), // size (arbitrary for our usage if we need to)
302  retval.tag, // extra tags
303  std::move(retval)); // constructor parameters
305 }
307 
311 {
312  assert(p_native && p_native->p_data_packs && p_native->pack_count > 0);
313 
315  const InternalParam<seal::Plaintext> &encoded_data =
316  this->getEngine().retrieveFromHandle<InternalParam<seal::Plaintext>>(h_encoded_data, InternalParamInfo::tagPlaintext);
318 
320  std::vector<std::vector<std::int64_t>> clear_result = m_p_workload->decodeResult(encoded_data.samples);
322 
324  hebench::APIBridge::DataPack &native_datapack = this->findDataPack(*p_native, encoded_data.param_position);
325 
326  std::uint64_t min_sample_count = std::min(native_datapack.buffer_count, clear_result.size());
327  for (std::uint64_t sample_i = 0; sample_i < min_sample_count; ++sample_i)
328  {
329  // alias the samples
330  hebench::APIBridge::NativeDataBuffer &native_sample = native_datapack.p_buffers[sample_i];
331  // copy as much as possible
332  const std::vector<std::int64_t> &decoded = clear_result[sample_i];
333  std::uint64_t min_size = std::min(decoded.size(), native_sample.size / sizeof(std::int64_t));
334  std::copy_n(decoded.begin(), min_size,
335  reinterpret_cast<std::int64_t *>(native_sample.p));
336  }
338 }
340 
343 {
345  const InternalParam<seal::Plaintext> &encoded_parameter =
346  this->getEngine().retrieveFromHandle<InternalParam<seal::Plaintext>>(h_encoded_parameters,
347  InternalParamInfo::tagPlaintext);
349 
351  std::vector<seal::Ciphertext> encrypted = m_p_workload->encryptVector(encoded_parameter.samples);
353 
355  InternalParam<seal::Ciphertext> retval;
356  retval.samples = std::move(encrypted);
357  retval.param_position = encoded_parameter.param_position;
358  retval.tag = InternalParamInfo::tagCiphertext;
359 
360  return this->getEngine().createHandle<decltype(retval)>(
361  sizeof(seal::Ciphertext) * retval.samples.size(), // size (arbitrary for our usage if we need to)
362  retval.tag, // extra tags
363  std::move(retval)); // constructor parameters
365 }
367 
370 {
372  const InternalParam<seal::Ciphertext> &encrypted_data =
373  this->getEngine().retrieveFromHandle<InternalParam<seal::Ciphertext>>(h_encrypted_data, InternalParamInfo::tagCiphertext);
375 
376  assert(encrypted_data.param_position == 0);
377 
379  std::vector<seal::Plaintext> encoded_data_samples = m_p_workload->decryptResult(encrypted_data.samples);
381 
383  InternalParam<seal::Plaintext> retval;
384  retval.samples = std::move(encoded_data_samples);
385  retval.param_position = encrypted_data.param_position;
386  retval.tag = InternalParamInfo::tagPlaintext;
387 
388  return this->getEngine().createHandle<decltype(retval)>(
389  sizeof(seal::Plaintext) * retval.samples.size(), // size (arbitrary for our usage if we need to)
390  retval.tag, // extra tags
391  std::move(retval)); // move to avoid copy
393 }
395 
398 {
399  assert(count == TutorialEltwiseAddBenchmarkDescription::ParametersCount);
400 
402  std::pair<std::vector<seal::Plaintext>, std::vector<seal::Ciphertext>> params;
404 
406  // We query for the parameter position, and, once found, we create a copy of the data.
407  for (std::size_t handle_i = 0; handle_i < count; ++handle_i)
408  {
409  // both our InternalParam<Plaintext> and InternalParam<Ciphertext> inherit from InternalParamInfo
410  const InternalParamInfo &param_info =
411  this->getEngine().retrieveFromHandle<InternalParamInfo>(p_local_data[handle_i]);
412  assert(param_info.param_position < TutorialEltwiseAddBenchmarkDescription::ParametersCount);
413 
414  switch (param_info.param_position)
415  {
416  case 0:
417  {
418  if (!params.first.empty())
419  throw hebench::cpp::HEBenchError(HEBERROR_MSG_CLASS("Duplicated operation parameter detected in input handle."),
421  // specialize to the correct InternalParam<?>
422  const InternalParam<seal::Plaintext> &internal_param =
423  this->getEngine().retrieveFromHandle<InternalParam<seal::Plaintext>>(p_local_data[handle_i],
424  InternalParamInfo::tagPlaintext);
425  // create a deep copy of input
426  params.first = internal_param.samples;
427  break;
428  }
429  case 1:
430  {
431  if (!params.second.empty())
432  throw hebench::cpp::HEBenchError(HEBERROR_MSG_CLASS("Duplicated operation parameter detected in input handle."),
434  // specialize to the correct InternalParam<?>
435  const InternalParam<seal::Ciphertext> &internal_param =
436  this->getEngine().retrieveFromHandle<InternalParam<seal::Ciphertext>>(p_local_data[handle_i],
437  InternalParamInfo::tagCiphertext);
438  // create a deep copy of input
439  params.second = internal_param.samples;
440  break;
441  }
442  } // end switch
443  } // end for
445 
447  return this->getEngine().createHandle<decltype(params)>(
448  sizeof(params), // size (arbitrary for our usage if we need to)
449  InternalParamInfo::tagPlaintext | InternalParamInfo::tagCiphertext, // extra tags
450  std::move(params)); // move to avoid extra copy
452 }
454 
457  hebench::APIBridge::Handle *p_local_data, std::uint64_t count)
458 {
459  if (count > 0)
460  {
462  std::memset(p_local_data, 0, sizeof(hebench::APIBridge::Handle) * count);
464 
466  p_local_data[0] = this->getEngine().duplicateHandle(h_remote_data,
467  InternalParamInfo::tagCiphertext); // validate that we are operating on the correct handle
469  }
470 }
472 
475  const hebench::APIBridge::ParameterIndexer *p_param_indexers)
476 {
478  const std::pair<std::vector<seal::Plaintext>, std::vector<seal::Ciphertext>> &params =
479  this->getEngine().retrieveFromHandle<std::pair<std::vector<seal::Plaintext>, std::vector<seal::Ciphertext>>>(
480  h_remote_packed, InternalParamInfo::tagCiphertext | InternalParamInfo::tagPlaintext);
481 
482  // Looks familiar?
483  const std::vector<seal::Plaintext> &A = params.first;
484  const std::vector<seal::Ciphertext> &B = params.second;
486 
488  std::array<std::size_t, TutorialEltwiseAddBenchmarkDescription::ParametersCount> param_size;
489  param_size[0] = A.size();
490  param_size[1] = B.size();
491  std::uint64_t results_count = 1;
492  for (std::size_t param_i = 0; param_i < TutorialEltwiseAddBenchmarkDescription::ParametersCount; ++param_i)
493  {
494  if (p_param_indexers[param_i].value_index >= param_size[param_i])
495  {
496  std::stringstream ss;
497  ss << "Invalid parameter indexer for operation parameter " << param_i << ". Expected index in range [0, "
498  << param_size[param_i] << "), but " << p_param_indexers[param_i].value_index << " received.";
501  }
502  else if (p_param_indexers[param_i].value_index + p_param_indexers[param_i].batch_size > param_size[param_i])
503  {
504  std::stringstream ss;
505  ss << "Invalid parameter indexer for operation parameter " << param_i << ". Expected batch size in range [1, "
506  << param_size[param_i] - p_param_indexers[param_i].value_index << "], but " << p_param_indexers[param_i].batch_size << " received.";
509  }
510  results_count *= p_param_indexers[param_i].batch_size; // count the number of results expected
511  }
513 
515  std::vector<seal::Ciphertext> result = m_p_workload->eltwiseadd(A, B);
516  assert(result.size() == results_count);
518 
520  // Finally, we wrap the result in our internal representation.
521  InternalParam<seal::Ciphertext> retval;
522  retval.samples = std::move(result);
523  retval.param_position = 0; // position of this result component inside the result tuple.
524  retval.tag = InternalParamInfo::tagCiphertext;
525 
526  // Hide our representation inside an opaque handle to cross the boundary of the API Bridge.
527  // This handle will be passed to method `store()` in the default pipeline.
528  return this->getEngine().createHandle<decltype(retval)>(
529  sizeof(seal::Ciphertext) * retval.samples.size(), // size (arbitrary for our usage if we need to)
530  retval.tag, // extra tags
531  std::move(retval)); // move to avoid extra copies
533 }
535 
Top level opaque benchmark class.
Definition: benchmark.hpp:143
Base class that encapsulates common behavior of backend engines.
Definition: engine.hpp:70
std::uint64_t n() const
Number elements in a vector.
#define HEBERROR_MSG_CLASS(message)
@ Int64
64 bits signed integers.
Definition: types.h:304
ErrorCode encrypt(Handle h_benchmark, Handle h_plaintext, Handle *h_ciphertext)
Encrypts a plain text into a cipher text.
ErrorCode operate(Handle h_benchmark, Handle h_remote_packed_params, const ParameterIndexer *p_param_indexers, uint64_t indexers_count, Handle *h_remote_output)
Performs the workload operation of the benchmark.
std::uint64_t count
Number of workload parameters.
Definition: types.h:371
DataPack * p_data_packs
Collection of data packs.
Definition: types.h:625
ErrorCode encode(Handle h_benchmark, const DataPackCollection *p_parameters, Handle *h_plaintext)
Given a pack of parameters in raw, native data format, encodes them into plain text suitable for back...
std::uint64_t value_index
Index of parameter value inside the data pack.
Definition: types.h:651
ErrorCode decrypt(Handle h_benchmark, Handle h_ciphertext, Handle *h_plaintext)
Decrypts a cipher text into corresponding plain text.
ErrorCode store(Handle h_benchmark, Handle h_remote, Handle *h_local_packed_params, std::uint64_t local_count)
Retrieves the specified data from the backend.
std::uint64_t pack_count
Number of data packs in the collection.
Definition: types.h:626
ErrorCode createBenchmark(Handle h_engine, Handle h_bench_desc, const WorkloadParams *p_params, Handle *h_benchmark)
Instantiates a benchmark on the backend.
std::uint64_t batch_size
Number of values to use, starting from index.
Definition: types.h:652
std::uint64_t size
Size of underlying data.
Definition: types.h:564
std::uint64_t param_position
The 0-based position of this parameter in the corresponding function call.
Definition: types.h:614
void * p
Pointer to underlying data.
Definition: types.h:558
ErrorCode load(Handle h_benchmark, const Handle *h_local_packed_params, std::uint64_t local_count, Handle *h_remote)
Loads the specified data from the local host into the remote backend to use as parameter during a cal...
std::uint64_t buffer_count
Number of data buffers in p_buffers.
Definition: types.h:613
ErrorCode decode(Handle h_benchmark, Handle h_plaintext, DataPackCollection *p_native)
Decodes plaintext data into the appropriate raw, native format.
NativeDataBuffer * p_buffers
Array of data buffers for parameter.
Definition: types.h:612
Workload
Defines all possible workloads.
Definition: types.h:83
Defines a benchmark test.
Definition: types.h:527
Defines a data package for an operation.
Definition: types.h:611
Defines a collection of data packs.
Definition: types.h:624
Specifies the parameters for a workload.
Definition: types.h:363
Structure to contain flexible data.
Definition: types.h:552
std::string to_string(const std::string_view &s)
#define HEBENCH_ECODE_INVALID_ARGS
Indicates invalid arguments to function call.
Definition: types.h:42
#define HEBENCH_ECODE_CRITICAL_ERROR
Specifies a critical error.
Definition: types.h:50
#define HEBENCH_HE_SCHEME_BFV
Definition: types.h:69