Imported Upstream version 1.25.0
[platform/core/ml/nnfw.git] / compiler / record-minmax / src / RecordMinMax.cpp
1 /*
2  * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *    http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 #include "RecordMinMax.h"
18 #include "MinMaxObserver.h"
19
20 #include <luci/Importer.h>
21 #include <luci/CircleExporter.h>
22 #include <luci/CircleFileExpContract.h>
23 #include <luci/IR/CircleQuantParam.h>
24 #include <luci/Log.h>
25 #include <dio_hdf5/HDF5Importer.h>
26
27 #include <dirent.h>
28 #include <algorithm>
29 #include <cmath>
30 #include <fstream>
31 #include <numeric>
32 #include <stdexcept>
33 #include <iostream>
34 #include <random>
35
36 using Shape = std::vector<loco::Dimension>;
37 using DataType = loco::DataType;
38
39 namespace
40 {
41
42 // Max h5 file size for parallel recording in bytes = 1 GB
43 const long h5_max_size_bytes = 1000000000;
44
45 long getH5FileSize(const std::string &input_data_path)
46 {
47   std::ifstream in_file(input_data_path, std::ios::binary);
48   in_file.seekg(0, std::ios::end);
49
50   return in_file.tellg();
51 }
52
53 uint32_t numElements(const luci::CircleNode *node)
54 {
55   uint32_t num_elements = 1;
56   for (uint32_t i = 0; i < node->rank(); i++)
57     num_elements *= node->dim(i).value();
58
59   return num_elements;
60 }
61
62 // Throw exception if input has one of the following conditions.
63 // 1. Have unknown dimension
64 // 2. Number of elements is 0
65 void checkInputDimension(const luci::CircleInput *input)
66 {
67   for (uint32_t i = 0; i < input->rank(); i++)
68     if (!input->dim(i).known())
69       throw std::runtime_error(input->name() + " has unknown dimension");
70
71   if (numElements(input) == 0)
72     throw std::runtime_error(input->name() + " is a zero-sized input");
73 }
74
75 void readDataFromFile(const std::string &filename, std::vector<char> &data, size_t data_size)
76 {
77   assert(data.size() == data_size); // FIX_CALLER_UNLESS
78
79   std::ifstream fs(filename, std::ifstream::binary);
80   if (fs.fail())
81     throw std::runtime_error("Cannot open file \"" + filename + "\".\n");
82   if (fs.read(data.data(), data_size).fail())
83     throw std::runtime_error("Failed to read data from file \"" + filename + "\".\n");
84   if (fs.peek() != EOF)
85     throw std::runtime_error("Input tensor size mismatches with \"" + filename + "\".\n");
86 }
87
88 std::vector<uint8_t> genRandomBoolData(std::mt19937 &gen, uint32_t num_elements)
89 {
90   std::uniform_int_distribution<> dist(0, 1);
91   std::vector<uint8_t> input_data(num_elements);
92
93   // Write random data
94   for (auto &iter : input_data)
95     iter = static_cast<uint8_t>(dist(gen));
96
97   return input_data;
98 }
99
100 template <typename T>
101 std::vector<T> genRandomIntData(std::mt19937 &gen, uint32_t num_elements, T min, T max)
102 {
103   std::uniform_int_distribution<T> dist(min, max);
104   std::vector<T> input_data(num_elements);
105
106   // Write random data
107   {
108     auto const generator = [&gen, &dist]() { return dist(gen); };
109     std::generate(begin(input_data), end(input_data), generator);
110   }
111
112   return input_data;
113 }
114
115 /**
116  * @brief  getTensorSize will return size in bytes
117  */
118 template <typename NodeT> size_t getTensorSize(const NodeT *node)
119 {
120   uint32_t tensor_size = loco::size(node->dtype());
121   for (uint32_t i = 0; i < node->rank(); ++i)
122     tensor_size *= node->dim(i).value();
123   return tensor_size;
124 }
125
126 /**
127  * @brief  verifyTypeShape checks the type and the shape of CircleInput
128  *         This throws an exception if type or shape does not match
129  */
130 void verifyTypeShape(const luci::CircleInput *input_node, const DataType &dtype, const Shape &shape)
131 {
132   // Type check
133   if (dtype != input_node->dtype())
134     throw std::runtime_error("Wrong input type.");
135
136   if (shape.size() != input_node->rank())
137     throw std::runtime_error("Input rank mismatch.");
138
139   for (uint32_t i = 0; i < shape.size(); i++)
140   {
141     if (not(shape.at(i) == input_node->dim(i)))
142       throw std::runtime_error("Input shape mismatch.");
143   }
144 }
145
146 } // namespace
147
148 namespace record_minmax
149 {
150
151 void RecordMinMax::initialize(const std::string &input_model_path)
152 {
153   assert(_threads_size > 0);
154
155   // Load model from the file
156   std::ifstream fs(input_model_path, std::ifstream::binary);
157   if (fs.fail())
158   {
159     throw std::runtime_error("Cannot open model file \"" + input_model_path + "\".\n");
160   }
161   std::vector<char> model_data((std::istreambuf_iterator<char>(fs)),
162                                std::istreambuf_iterator<char>());
163
164   // Verify flatbuffers
165   flatbuffers::Verifier verifier{reinterpret_cast<const uint8_t *>(model_data.data()),
166                                  model_data.size()};
167   if (!circle::VerifyModelBuffer(verifier))
168   {
169     throw std::runtime_error("Failed to verify circle '" + input_model_path + "'");
170   }
171
172   const circle::Model *circle_model = circle::GetModel(model_data.data());
173   if (circle_model == nullptr)
174   {
175     throw std::runtime_error("Failed to load '" + input_model_path + "'");
176   }
177
178   _module = luci::Importer().importModule(circle_model);
179
180   if (_module == nullptr)
181   {
182     throw std::runtime_error("Failed to load '" + input_model_path + "'");
183   }
184
185   // Create and initialize interpreters and observers
186   _interpreters.resize(_threads_size);
187   _observers.resize(_threads_size);
188
189   for (uint32_t thread_idx = 0; thread_idx < _threads_size; ++thread_idx)
190   {
191     auto interpreter = std::make_unique<luci_interpreter::Interpreter>(_module.get());
192     auto observer = std::make_unique<MinMaxObserver>();
193
194     interpreter->attachObserver(observer.get());
195
196     _observers[thread_idx] = std::move(observer);
197     _interpreters[thread_idx] = std::move(interpreter);
198   }
199 }
200
201 // input_data_path is a path to the directory
202 // The directory should contain binary files each of which is a raw data,
203 // ready to be consumed by the input circle model without any modification
204 // TODO reduce duplicate codes with profileRawData
205 void RecordMinMax::profileRawDataDirectory(const std::string &input_data_path)
206 {
207   struct dirent *entry = nullptr;
208   DIR *dp = nullptr;
209
210   dp = opendir(input_data_path.c_str());
211   if (not dp)
212     throw std::runtime_error("Cannot open directory. Please check \"" + input_data_path +
213                              "\" is a directory.\n");
214
215   uint32_t num_records = 0;
216   const auto input_nodes = loco::input_nodes(_module->graph());
217
218   // Get total input size
219   uint32_t total_input_size = 0;
220   for (auto input : input_nodes)
221   {
222     const auto *input_node = loco::must_cast<const luci::CircleInput *>(input);
223     checkInputDimension(input_node);
224     total_input_size += getTensorSize(input_node);
225   }
226
227   while ((entry = readdir(dp)))
228   {
229     // Skip if the entry is not a regular file
230     if (entry->d_type != DT_REG)
231       continue;
232
233     const std::string filename = entry->d_name;
234     std::cout << "Recording " << num_records << "'th data" << std::endl;
235
236     // Read data from file to buffer
237     // Assumption: For a multi-input model, the binary file should have inputs concatenated in the
238     // same order with the input index.
239     std::vector<char> input_data(total_input_size);
240     readDataFromFile(input_data_path + "/" + filename, input_data, total_input_size);
241
242     // Write data from buffer to interpreter
243     uint32_t offset = 0;
244     for (auto input : input_nodes)
245     {
246       const auto *input_node = loco::must_cast<const luci::CircleInput *>(input);
247       const auto input_size = getTensorSize(input_node);
248       getInterpreter()->writeInputTensor(input_node, input_data.data() + offset, input_size);
249
250       offset += input_size;
251     }
252
253     getInterpreter()->interpret();
254
255     num_records++;
256   }
257
258   closedir(dp);
259
260   if (num_records == 0)
261     throw std::runtime_error("The input data file does not contain any record.");
262
263   std::cout << "Recording finished. Number of recorded data: " << num_records << std::endl;
264
265   _minmax_computer->update_qparam(getObserver()->minMaxData()->getMap());
266 }
267
268 // input_data_path is a text file which specifies the representative data
269 // The text file should contain absolute file path per line.
270 // The pointed file should be a binary file containing one representative data,
271 // ready to be consumed by the input circle model without any modification
272 // NOTE If a model has multiple inputs, the binary file should have inputs concatenated in the same
273 // order with the input index of the circle model.
274 void RecordMinMax::profileRawData(const std::string &input_data_path)
275 {
276   std::ifstream input_file(input_data_path);
277   if (input_file.fail())
278     throw std::runtime_error("Cannot open file \"" + input_data_path + "\".\n");
279
280   std::string record;
281   uint32_t num_records = 0;
282   const auto input_nodes = loco::input_nodes(_module->graph());
283
284   // Get total input size
285   uint32_t total_input_size = 0;
286   for (auto input : input_nodes)
287   {
288     const auto *input_node = loco::must_cast<const luci::CircleInput *>(input);
289     checkInputDimension(input_node);
290     total_input_size += getTensorSize(input_node);
291   }
292
293   while (getline(input_file, record))
294   {
295     std::cout << "Recording " << num_records << "'th data" << std::endl;
296
297     // Read data from file to buffer
298     // Assumption: For a multi-input model, the binary file should have inputs concatenated in the
299     // same order with the input index.
300     std::vector<char> input_data(total_input_size);
301     readDataFromFile(record, input_data, total_input_size);
302
303     // Write data from buffer to interpreter
304     uint32_t offset = 0;
305     for (auto input : input_nodes)
306     {
307       const auto *input_node = loco::must_cast<const luci::CircleInput *>(input);
308       const auto input_size = getTensorSize(input_node);
309       getInterpreter()->writeInputTensor(input_node, input_data.data() + offset, input_size);
310
311       offset += input_size;
312     }
313
314     getInterpreter()->interpret();
315
316     num_records++;
317   }
318
319   if (num_records == 0)
320     throw std::runtime_error("The input data file does not contain any record.");
321
322   std::cout << "Recording finished. Number of recorded data: " << num_records << std::endl;
323
324   _minmax_computer->update_qparam(getObserver()->minMaxData()->getMap());
325 }
326
327 WholeOutput RecordMinMax::importH5Data(const std::string &input_data_path)
328 {
329   try
330   {
331     dio::hdf5::HDF5Importer importer(input_data_path);
332     importer.importGroup("value");
333
334     bool is_raw_data = importer.isRawData();
335
336     const auto num_records = importer.numData();
337     if (num_records == 0)
338       throw std::runtime_error("The input data file does not contain any record.");
339
340     const auto input_nodes = loco::input_nodes(_module->graph());
341     const auto num_inputs = input_nodes.size();
342
343     WholeOutput whole_output(num_records);
344
345     // Read inputs to whole_output
346     for (int i = 0; i < num_records; ++i)
347     {
348       if (num_inputs != static_cast<uint32_t>(importer.numInputs(i)))
349         throw std::runtime_error("Wrong number of inputs.");
350
351       for (uint32_t input_idx = 0; input_idx < num_inputs; input_idx++)
352       {
353         const auto *input_node = loco::must_cast<const luci::CircleInput *>(input_nodes[input_idx]);
354         assert(input_node->index() == input_idx);
355         checkInputDimension(input_node);
356         Buffer input_data(getTensorSize(input_node));
357
358         if (!is_raw_data)
359         {
360           DataType dtype;
361           Shape shape;
362           importer.readTensor(i, input_idx, &dtype, &shape, input_data.data(), input_data.size());
363
364           // Check the type and the shape of the input data is valid
365           verifyTypeShape(input_node, dtype, shape);
366         }
367         else
368         {
369           // Skip type/shape check for raw data
370           importer.readTensor(i, input_idx, input_data.data(), input_data.size());
371         }
372         whole_output[i].emplace_back(std::move(input_data));
373       }
374     }
375
376     return whole_output;
377   }
378   catch (const H5::Exception &e)
379   {
380     H5::Exception::printErrorStack();
381     throw std::runtime_error("HDF5 error occurred.");
382   }
383 }
384
385 void RecordMinMax::profileData(const std::string &input_data_path)
386 {
387   try
388   {
389     dio::hdf5::HDF5Importer importer(input_data_path);
390     importer.importGroup("value");
391
392     bool is_raw_data = importer.isRawData();
393
394     const auto num_records = importer.numData();
395     if (num_records == 0)
396       throw std::runtime_error("The input data file does not contain any record.");
397
398     const auto input_nodes = loco::input_nodes(_module->graph());
399     const auto num_inputs = input_nodes.size();
400
401     for (int32_t record_idx = 0; record_idx < num_records; record_idx++)
402     {
403       if (num_inputs != static_cast<uint32_t>(importer.numInputs(record_idx)))
404         throw std::runtime_error("Wrong number of inputs.");
405
406       std::cout << "Recording " << record_idx << "'th data" << std::endl;
407
408       for (uint32_t input_idx = 0; input_idx < num_inputs; input_idx++)
409       {
410         const auto *input_node = loco::must_cast<const luci::CircleInput *>(input_nodes[input_idx]);
411         assert(input_node->index() == input_idx);
412         checkInputDimension(input_node);
413         std::vector<char> input_data(getTensorSize(input_node));
414
415         if (!is_raw_data)
416         {
417           DataType dtype;
418           Shape shape;
419           importer.readTensor(record_idx, input_idx, &dtype, &shape, input_data.data(),
420                               input_data.size());
421
422           // Check the type and the shape of the input data is valid
423           verifyTypeShape(input_node, dtype, shape);
424         }
425         else
426         {
427           // Skip type/shape check for raw data
428           importer.readTensor(record_idx, input_idx, input_data.data(), input_data.size());
429         }
430
431         // TODO: Input data is copied twice (file -> buffer (input_data) -> interpreter inputs)
432         //       We can redcue the copy by directly writing data from file to interpreter inputs
433         getInterpreter()->writeInputTensor(input_node, input_data.data(), input_data.size());
434       }
435
436       getInterpreter()->interpret();
437     }
438
439     std::cout << "Recording finished. Number of recorded data: " << num_records << std::endl;
440   }
441   catch (const H5::Exception &e)
442   {
443     H5::Exception::printErrorStack();
444     throw std::runtime_error("HDF5 error occurred.");
445   }
446
447   _minmax_computer->update_qparam(getObserver()->minMaxData()->getMap());
448 }
449
450 void RecordMinMax::profileDataInParallel(const std::string &input_data_path)
451 {
452   LOGGER(l);
453
454   assert(_interpreters.size() == _threads_size);
455   assert(_observers.size() == _threads_size);
456
457   const long h5_file_size = getH5FileSize(input_data_path);
458
459   if (h5_file_size > h5_max_size_bytes)
460     throw std::runtime_error("H5 file size is too large for parallel recording");
461
462   WholeOutput whole_output;
463   try
464   {
465     whole_output = importH5Data(input_data_path);
466   }
467   catch (const std::bad_alloc &e)
468   {
469     throw std::runtime_error("Out of memory during h5 data load.");
470   }
471
472   const auto num_records = whole_output.size();
473   const auto input_nodes = loco::input_nodes(_module->graph());
474
475   // Start parallel part
476   INFO(l) << _threads_size << " concurrent threads are supported." << std::endl;
477
478   const auto run_threads = num_records < _threads_size ? num_records : _threads_size;
479
480   const auto records_batch = static_cast<uint32_t>(num_records / run_threads);
481
482   auto interpret_batch = [&whole_output, &input_nodes](int first_record, int last_record,
483                                                        luci_interpreter::Interpreter *interpreter) {
484     for (int record_index = first_record; record_index < last_record; ++record_index)
485     {
486       for (uint32_t input_idx = 0; input_idx < input_nodes.size(); input_idx++)
487       {
488         const auto *input_node = loco::must_cast<const luci::CircleInput *>(input_nodes[input_idx]);
489
490         const auto &cur_input_data = whole_output[record_index][input_idx];
491         interpreter->writeInputTensor(input_node, cur_input_data.data(), cur_input_data.size());
492       }
493       interpreter->interpret();
494     }
495   };
496
497   std::vector<std::thread> threads;
498   for (uint32_t t = 0; t < run_threads; ++t)
499   {
500     if (t < run_threads - 1)
501     {
502       threads.emplace_back(interpret_batch, records_batch * t, records_batch * (t + 1),
503                            _interpreters[t].get());
504     }
505     else
506     {
507       threads.emplace_back(interpret_batch, records_batch * t, num_records, _interpreters[t].get());
508     }
509   }
510
511   for (uint32_t i = 0; i < run_threads; ++i)
512     threads.at(i).join();
513
514   // End parallel part
515
516   // Copy all min, max values to one min/max map
517   MinMaxMap main_min_max_map;
518
519   for (const auto &obs : _observers)
520   {
521     const auto cur_minmax_map = obs->minMaxData()->getMap();
522     for (auto &iter : *cur_minmax_map)
523     {
524       const auto node = iter.first;
525       const auto &minmax = iter.second;
526
527       main_min_max_map.appendMinMaxVector(node, minmax);
528     }
529   }
530
531   std::cout << "Recording finished. Number of recorded data: " << num_records << std::endl;
532
533   _minmax_computer->update_qparam(main_min_max_map.getMap());
534 }
535
536 void RecordMinMax::profileDataWithRandomInputs(void)
537 {
538   // We use three randomly-generated records
539   const uint32_t num_records = 3;
540
541   const auto input_nodes = loco::input_nodes(_module->graph());
542   const auto num_inputs = input_nodes.size();
543
544   std::random_device rd;
545   std::mt19937 gen(rd());
546   std::uniform_real_distribution<> dist(-5, 5);
547
548   for (uint32_t record_idx = 0; record_idx < num_records; record_idx++)
549   {
550     std::cout << "Recording " << record_idx << "'th data" << std::endl;
551
552     for (uint32_t input_idx = 0; input_idx < num_inputs; input_idx++)
553     {
554       const auto *input_node = loco::must_cast<const luci::CircleInput *>(input_nodes[input_idx]);
555       assert(input_node->index() == input_idx);
556       checkInputDimension(input_node);
557
558       const auto num_elements = numElements(input_node);
559
560       // TODO Support more input data types
561       assert(input_node->dtype() == loco::DataType::FLOAT32 ||
562              input_node->dtype() == loco::DataType::BOOL ||
563              input_node->dtype() == loco::DataType::S32 ||
564              input_node->dtype() == loco::DataType::S64);
565
566       if (input_node->dtype() == DataType::FLOAT32)
567       {
568         std::vector<float> input_data(num_elements);
569
570         // Write random data
571         for (auto &iter : input_data)
572           iter = static_cast<float>(dist(gen));
573
574         // TODO: Input data is copied twice (file -> buffer (input_data) -> interpreter inputs)
575         //       We can redcue the copy by directly writing data from file to interpreter inputs
576         getInterpreter()->writeInputTensor(input_node, input_data.data(),
577                                            input_data.size() * sizeof(float));
578       }
579       else if (input_node->dtype() == DataType::BOOL)
580       {
581         auto input_data = genRandomBoolData(gen, num_elements);
582         getInterpreter()->writeInputTensor(input_node, input_data.data(),
583                                            input_data.size() * sizeof(uint8_t));
584       }
585       else if (input_node->dtype() == DataType::S32)
586       {
587         auto input_data = genRandomIntData<int32_t>(gen, num_elements, 0, 100);
588         getInterpreter()->writeInputTensor(input_node, input_data.data(),
589                                            input_data.size() * sizeof(int32_t));
590       }
591       else if (input_node->dtype() == DataType::S64)
592       {
593         auto input_data = genRandomIntData<int64_t>(gen, num_elements, 0, 100);
594         getInterpreter()->writeInputTensor(input_node, input_data.data(),
595                                            input_data.size() * sizeof(int64_t));
596       }
597     }
598
599     getInterpreter()->interpret();
600   }
601
602   std::cout << "Recording finished. Number of recorded data: " << num_records << std::endl;
603
604   _minmax_computer->update_qparam(getObserver()->minMaxData()->getMap());
605 }
606
607 void RecordMinMax::saveModel(const std::string &output_model_path)
608 {
609   // Export to output Circle file
610   luci::CircleExporter exporter;
611
612   luci::CircleFileExpContract contract(_module.get(), output_model_path);
613
614   if (!exporter.invoke(&contract))
615   {
616     throw std::runtime_error("Failed to export '" + output_model_path + "'");
617   }
618 }
619
620 } // namespace record_minmax