[tct] fix coverity issues
[platform/core/ml/nntrainer.git] / nntrainer / compiler / tflite_interpreter.cpp
1 // SPDX-License-Identifier: Apache-2.0
2 /**
3  * Copyright (C) 2021 Jihoon Lee <jhoon.it.lee@samsung.com>
4  *
5  * @file tflite_interpreter.cpp
6  * @date 12 April 2021
7  * @brief NNTrainer *.tflite Interpreter
8  * @see https://github.com/nnstreamer/nntrainer
9  * @author Jihoon Lee <jhoon.it.lee@samsung.com>
10  * @bug No known bugs except for NYI items
11  */
12 #include <tflite_interpreter.h>
13
14 #include <algorithm>
15 #include <fstream>
16 #include <map>
17 #include <memory>
18 #include <set>
19 #include <string>
20 #include <tuple>
21 #include <type_traits>
22 #include <utility>
23
24 #include <bn_realizer.h>
25 #include <fc_layer.h>
26 #include <layer_node.h>
27 #include <loss_realizer.h>
28 #include <nntrainer_error.h>
29 #include <node_exporter.h>
30 #include <tensor.h>
31 #include <tf_schema_generated.h>
32 #include <tflite_opnode.h>
33
34 static constexpr const char *FUNC_TAG = "[TFLITE INTERPRETER] ";
35
36 namespace nntrainer {
37
38 namespace {
39 /**
40  * @brief after finishing building, call this to safe to a file
41  *
42  * @param builder flatbuffer builder
43  * @param out out
44  */
45 void builder2file(const flatbuffers::FlatBufferBuilder &builder,
46                   const std::string &out) {
47   uint8_t *buf = builder.GetBufferPointer();
48   size_t size = builder.GetSize();
49   flatbuffers::Verifier v(buf, size);
50
51   NNTR_THROW_IF(!tflite::VerifyModelBuffer(v), std::invalid_argument)
52     << FUNC_TAG << "Verifying serialized model failed";
53
54   std::ofstream os(out, std::ios_base::binary);
55   const size_t error_buflen = 100;
56   char error_buf[error_buflen];
57   NNTR_THROW_IF(!os.good(), std::invalid_argument)
58     << FUNC_TAG
59     << "failed to open, reason: " << strerror_r(errno, error_buf, error_buflen);
60
61   std::streamsize sz = static_cast<std::streamsize>(builder.GetSize());
62   NNTR_THROW_IF(sz < 0, std::invalid_argument)
63     << FUNC_TAG << "builder size: " << builder.GetSize()
64     << " is too big. It cannot be represented by std::streamsize";
65
66   os.write((char *)builder.GetBufferPointer(), sz);
67   os.close();
68 }
69
70 /**
71  * @brief get predecessor nodes
72  *
73  * @param node the node from which to get predecessor nodes
74  * @note virtual nodes are ignored
75  */
76 std::vector<const TfOpNode *> getPredNodes(const TfOpNode &node) {
77   std::vector<const TfOpNode *> predNodes;
78
79   for (auto input : node.getInputNodes()) {
80     const TfOpNode *pred = input;
81     while (pred->isVirtualNode()) {
82       /// Assume that virtual nodes have single input
83       assert(pred->arity() == 1);
84       pred = pred->arg(0);
85     }
86     predNodes.push_back(pred);
87   }
88   return predNodes;
89 }
90
91 using TfOpNodes = std::vector<std::unique_ptr<TfOpNode>>;
92
93 /**
94  * @brief Bidirectional Index map
95  *
96  * @tparam Key type of a underlying hashable value, please note that T will be
97  * copied, so please use this for pointers and primitive values that is okay to
98  * copy
99  * @tparam Data data type to be stored inside the vector, if not given, same as
100  * KeyType
101  */
102 template <typename KeyType, typename DataType = KeyType>
103 class BidirectionalIndexMap {
104 public:
105   /**
106    * @brief addDatapoint to the map
107    *
108    * @param key key to be added to search for the data
109    * @param data data to be added if there is no occurrence, data will be
110    * copied.
111    */
112   void addDataWhenNotFound(KeyType key, DataType data) {
113     auto search = key2index.find(key);
114
115     if (search == key2index.end()) {
116       key2index[key] = index2data.size();
117       index2data.push_back(data);
118     }
119   }
120
121   /**
122    * @brief addDatapoint to the map when key and datatype is same
123    *
124    * @param key key/data to add
125    */
126   void addDataWhenNotFound(KeyType key) {
127     static_assert(std::is_same<KeyType, DataType>::value == true,
128                   "key type and data type are different!");
129     addDataWhenNotFound(key, key);
130   }
131
132   /**
133    * @brief Get the Index of the data
134    *
135    * @param key data that will be the key
136    * @return unsigned int index
137    */
138   unsigned int getIndex(const KeyType &key) const {
139     auto search = key2index.find(key);
140
141     NNTR_THROW_IF(search == key2index.end(), std::invalid_argument)
142       << FUNC_TAG << "Cannot find index for key: " << key;
143
144     return search->second;
145   }
146
147   /**
148    * @brief Get the Data object
149    *
150    * @param idx index to be searched
151    * @return T datapoint T
152    */
153   DataType getData(unsigned int index) const {
154     NNTR_THROW_IF(index >= index2data.size(), std::invalid_argument)
155       << FUNC_TAG << "Cannot find data for index: " << index;
156
157     return index2data[index];
158   }
159
160   /**
161    * @brief Get the Data object
162    *
163    * @return const std::vector<T>& underlying data
164    */
165   const std::vector<DataType> &getData() const { return index2data; }
166
167 private:
168   std::unordered_map<KeyType, unsigned int> key2index; /**< key -> index map */
169   std::vector<DataType> index2data;                    /**< index -> data map */
170 };
171
172 /**
173  * @brief tensorflow operation index map, this class manages operation index
174  * mapping
175  *
176  */
177 class TfOpIdxMap {
178 public:
179   using Buffer = std::pair<size_t, const float *>;
180
181   TfOpIdxMap(const TfOpNodes &nodes) {
182     auto &opcode_map = getIndexMap<tflite::BuiltinOperator>();
183     auto update_opcode = [&opcode_map](tflite::BuiltinOperator opcode) {
184       opcode_map.addDataWhenNotFound(opcode);
185     };
186
187     auto &buffer_map = getIndexMap<const float *, Buffer>();
188     buffer_map.addDataWhenNotFound(
189       nullptr, {0, empty_buffer}); // this represents undefined buffer
190     buffer_map.addDataWhenNotFound(
191       empty_buffer, {0, empty_buffer}); /// this represents empty buffer
192
193     auto update_buffer_map = [&buffer_map](const TfOpNode::Variables &variables,
194                                            bool dynamic) {
195       for (auto &variable : variables) {
196         const float *buf = variable->getData();
197         assert(buf != nullptr);
198         auto byte_size = dynamic ? 0 : variable->bytes();
199         buffer_map.addDataWhenNotFound(buf, {byte_size, buf});
200       }
201     };
202
203     auto register_tensors =
204       [&tensors = this->tensors](const TfOpNode::Variables &variables) {
205         for (auto &variable : variables) {
206           auto tensor_it = std::find(tensors.begin(), tensors.end(), variable);
207           if (tensor_it == tensors.end()) {
208             tensors.push_back(variable);
209           }
210         }
211       };
212
213     for (auto &op_node : nodes) {
214       if (op_node->isVirtualNode())
215         continue;
216       update_opcode(op_node->getOpType());
217
218       if (op_node->isInputNode()) {
219         /**
220          * Q) Why only register graph input tensor?
221          *
222          * A) the tflite needs only one tensor between nodes. Therefore,
223          *basically, no inputs are considered except graph input that doesn't
224          *have FROM node.
225          **/
226         register_tensors(op_node->getInputs());
227         /**
228          * Q) Why only update second input of the input node?
229          *
230          * A) 1. graph input nodes should be Transpose operator to change data
231          *format from NCHW to NHWC.
232          *    2. Transpose operator has two inputs - input to be
233          *transposed(input[0]), 1d permute vector(input[1])
234          *    3. input[0] has nullptr data pointer, which can't be added to
235          *buffer_map. But, input[0] should have its own buffer and it will be
236          *considered when the tflite buffers are built.
237          **/
238         assert(op_node->getInputs()[0]->getData() == nullptr);
239         update_buffer_map({op_node->getInputs()[1]}, false);
240       }
241       register_tensors(op_node->getWeights());
242       update_buffer_map(op_node->getWeights(), false);
243
244       register_tensors(op_node->getOutputs());
245       update_buffer_map(op_node->getOutputs(), true);
246     }
247
248     auto update_model_io_to = [this](const TfOpNode::Variables &variables,
249                                      std::vector<int> &v) {
250       for (auto &variable : variables) {
251         if (variable->getName().find("nntrainer_internal_perm") !=
252             std::string::npos)
253           continue;
254         v.push_back(this->getTensorIndex(variable));
255       }
256     };
257
258     for (auto &op_node : nodes) {
259       if (op_node->isVirtualNode())
260         continue;
261       if (op_node->isInputNode()) {
262         update_model_io_to(op_node->getInputs(), inputs);
263       }
264       if (op_node->isOutputNode()) {
265         update_model_io_to(op_node->getOutputs(), outputs);
266       }
267     }
268   }
269
270   template <typename KeyType, typename DataType = KeyType>
271   BidirectionalIndexMap<KeyType, DataType> &getIndexMap() {
272     return std::get<BidirectionalIndexMap<KeyType, DataType>>(maps);
273   }
274
275   template <typename KeyType, typename DataType = KeyType>
276   const BidirectionalIndexMap<KeyType, DataType> &getIndexMap() const {
277     return std::get<BidirectionalIndexMap<KeyType, DataType>>(maps);
278   }
279
280   const float *get_empty_buffer() const { return empty_buffer; }
281
282   const std::vector<int> &getInputs() const { return inputs; }
283
284   const std::vector<int> &getOutputs() const { return outputs; }
285
286   const std::vector<const Tensor *> &getTensors() const { return tensors; }
287
288   std::ptrdiff_t getTensorIndex(const Tensor *tensor) const {
289     auto tensor_it = std::find(tensors.begin(), tensors.end(), tensor);
290     NNTR_THROW_IF(tensor_it == tensors.cend(), std::invalid_argument)
291       << FUNC_TAG << "Cannot find index for tensor: " << tensor->getName();
292     return std::distance(tensors.begin(), tensor_it);
293   }
294
295 private:
296   float empty_buffer[0]; /**< reserved uninitialized tensor points to this
297                             buffer */
298
299   std::tuple<BidirectionalIndexMap<const float *, Buffer>,   /**< buffer map
300                                                               */
301              BidirectionalIndexMap<tflite::BuiltinOperator>> /**< opcode map
302                                                               */
303     maps;
304
305   std::vector<int> inputs;
306   std::vector<int> outputs;
307   /// since it is used as a tensor index, the order is important
308   std::vector<const Tensor *> tensors;
309 };
310
311 TfOpNodes buildOpNodes(const GraphRepresentation &representation,
312                        flatbuffers::FlatBufferBuilder &fbb) {
313   TfOpNodes nodes;
314   /// @todo TfOpNode needs to have LayerNode pointer
315   std::map<TfOpNode *, const LayerNode *> tf_to_layer;
316   std::map<const LayerNode *, TfOpNode *> layer_to_tf;
317
318   /// @todo, look ahead of layers to get nodes that can be fused
319   /// we will need to have a dedicated builder
320   for (auto iter = representation.cbegin(); iter != representation.cend();
321        iter++) {
322     const auto &ln = *iter;
323
324     Exporter e(&fbb);
325     ln->exportTo(e, ml::train::ExportMethods::METHOD_TFLITE);
326
327     nodes.emplace_back(e.getResult<ml::train::ExportMethods::METHOD_TFLITE>());
328     tf_to_layer.insert({nodes.back().get(), ln.get()});
329     layer_to_tf.insert({ln.get(), nodes.back().get()});
330   }
331
332   int node_count = 0;
333   bool is_local_first = true;
334   /** is_local_first : first FC Layer after Channel related layer
335    * For example
336    * : Input -> Conv -> Conv -> Flatten -> [FC]:local_first
337    * : Input -> Conv -> Flatten -> [FC]:local_first -> Conv -> Flatten ->
338    * [FC]:local_first
339    */
340
341   for (auto &n : nodes) {
342     auto tf_node = n.get();
343
344     if (tf_node->getOptionType() ==
345           tflite::BuiltinOptions::BuiltinOptions_FullyConnectedOptions &&
346         node_count != 0 && is_local_first) {
347       tf_node->setNeedReorderWeight();
348       is_local_first = false;
349     }
350
351     if (is_local_first == false &&
352         tf_node->getOptionType() !=
353           tflite::BuiltinOptions::BuiltinOptions_FullyConnectedOptions) {
354       is_local_first = true;
355     }
356
357     node_count++;
358   }
359
360   /// set arity of TfOpNodes
361   for (auto &n : nodes) {
362     auto tf_node = n.get();
363     auto searched_layer = tf_to_layer.find(tf_node);
364     if (searched_layer == tf_to_layer.end())
365       throw std::runtime_error("Cannot find layer for TfOpNode");
366     auto layer_node = searched_layer->second;
367     auto layer_node_inputs = layer_node->getInputConnections();
368
369     /// assume that the TfOpNode and the LayerNode have a one-to-one
370     /// relationship
371     tf_node->arity(layer_node_inputs.size());
372     for (size_t index = 0; index < layer_node_inputs.size(); index++) {
373       auto input_layer_name = layer_node_inputs[index];
374       auto input_later_node_iterator = std::find_if(
375         representation.begin(), representation.end(),
376         [&input_layer_name](std::shared_ptr<nntrainer::LayerNode> node) {
377           return istrequal(node.get()->getName(), input_layer_name);
378         });
379
380       if (input_later_node_iterator != representation.end()) {
381         auto input_layer_node = input_later_node_iterator->get();
382         if (layer_to_tf.find(input_layer_node) != layer_to_tf.end()) {
383           tf_node->setArg(index, layer_to_tf.find(input_layer_node)->second);
384         }
385       }
386     }
387   }
388
389   node_count = 0;
390   for (auto &n : nodes) {
391     auto tf_node = n.get();
392     if (tf_node->getOptionType() ==
393         tflite::BuiltinOptions::BuiltinOptions_FullyConnectedOptions) {
394       tf_node->weightReorder(node_count);
395     }
396
397     node_count++;
398   }
399
400   return nodes;
401 }
402
403 flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<tflite::Buffer>>>
404 buildBuffers(const TfOpIdxMap &map, flatbuffers::FlatBufferBuilder &fbb) {
405   const auto &buffers =
406     map.getIndexMap<const float *, TfOpIdxMap::Buffer>().getData();
407
408   std::vector<flatbuffers::Offset<tflite::Buffer>> fb_buffers;
409   fb_buffers.reserve(buffers.size());
410
411   auto create_buffer_offset = [&fbb](const TfOpIdxMap::Buffer &buffer) {
412     if (buffer.first == 0) {
413       return tflite::CreateBuffer(fbb);
414     }
415
416     auto data = fbb.CreateVector(
417       reinterpret_cast<const uint8_t *>(buffer.second), buffer.first);
418
419     return tflite::CreateBuffer(fbb, data);
420   };
421
422   std::transform(buffers.begin(), buffers.end(), std::back_inserter(fb_buffers),
423                  create_buffer_offset);
424
425   // add input buffer
426   for (unsigned index = 0; index < map.getInputs().size(); index++) {
427     fb_buffers.push_back(create_buffer_offset({0, nullptr}));
428   }
429   return fbb.CreateVector(fb_buffers);
430 }
431
432 flatbuffers::Offset<
433   flatbuffers::Vector<flatbuffers::Offset<tflite::OperatorCode>>>
434 buildOperatorCodes(const TfOpIdxMap &map, flatbuffers::FlatBufferBuilder &fbb) {
435   const auto &op_codes = map.getIndexMap<tflite::BuiltinOperator>().getData();
436
437   std::vector<flatbuffers::Offset<tflite::OperatorCode>> fb_op_codes;
438   fb_op_codes.reserve(op_codes.size());
439
440   auto create_op_offset = [&fbb](const tflite::BuiltinOperator &op,
441                                  int32_t version = 1) {
442     tflite::OperatorCodeBuilder builder(fbb);
443     builder.add_deprecated_builtin_code(static_cast<int8_t>(op));
444     /// @todo find reason why version field is not shown
445     /// on json when version is 1 (other versions are fine)
446     builder.add_version(version);
447     builder.add_builtin_code(op);
448     return builder.Finish();
449   };
450
451   std::transform(op_codes.begin(), op_codes.end(),
452                  std::back_inserter(fb_op_codes), create_op_offset);
453
454   return fbb.CreateVector(fb_op_codes);
455 }
456
457 flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<tflite::Tensor>>>
458 buildTensors(const TfOpIdxMap &map, flatbuffers::FlatBufferBuilder &fbb) {
459   /// @todo: the actual (suqeezed) tensor dimension must be known before
460   /// coming here. For now, it is directly guessed for the fc layer
461   const auto &variables = map.getTensors();
462   const auto &buffer_map = map.getIndexMap<const float *, TfOpIdxMap::Buffer>();
463   auto graph_input_offset = map.getInputs().size() - 1;
464
465   std::vector<flatbuffers::Offset<tflite::Tensor>> fb_tensors;
466   fb_tensors.reserve(variables.size());
467
468   auto create_tensor = [&fbb, &buffer_map,
469                         &graph_input_offset](const Tensor *var) {
470     auto dim = var->getDim();
471     bool need_shape_signature = dim.is_dynamic();
472     std::vector<int32_t> eff_dim = dim.getEffectiveDimension();
473     auto shape = fbb.CreateVector(eff_dim);
474
475     decltype(shape) shape_sig;
476     if (need_shape_signature) {
477       std::vector<int32_t> dyn_dim = dim.getEffectiveDimension(true);
478       shape_sig = fbb.CreateVector(dyn_dim);
479     }
480
481     /// change this var->getName when tensor have it's own name
482     auto name = fbb.CreateString("nntrainer_converted" + var->getName());
483
484     /// only graph inputs have nullptr data pointer.
485     unsigned int buffer_idx =
486       var->getData() == nullptr
487         ? buffer_map.getData().size() - graph_input_offset--
488         : buffer_map.getIndex(var->getData());
489
490     tflite::TensorBuilder builder(fbb);
491     builder.add_name(name);
492     builder.add_buffer(buffer_idx);
493     /// @todo support more data types
494     /// @note this is workaround because nntrainer tensor allows only float
495     /// dtype
496     if (var->getName().find("nntrainer_internal_perm") != std::string::npos) {
497       builder.add_type(tflite::TensorType_INT32);
498     } else
499       builder.add_type(tflite::TensorType_FLOAT32);
500     builder.add_shape(shape);
501     if (need_shape_signature) {
502       builder.add_shape_signature(shape_sig);
503     }
504     return builder.Finish();
505   };
506
507   std::transform(variables.begin(), variables.end(),
508                  std::back_inserter(fb_tensors), create_tensor);
509
510   return fbb.CreateVector(fb_tensors);
511 }
512
513 flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<tflite::Operator>>>
514 buildOperators(const TfOpNodes &nodes, const TfOpIdxMap &map,
515                flatbuffers::FlatBufferBuilder &fbb) {
516
517   /// this lambda maps variables to list of indexes in the map
518   auto variables_to_idx_vector = [&map](const TfOpNode::Variables &v) {
519     std::vector<int> idx_vector;
520     idx_vector.reserve(v.size());
521
522     std::transform(
523       v.begin(), v.end(), std::back_inserter(idx_vector),
524       [&map](const Tensor *variable) { return map.getTensorIndex(variable); });
525     return idx_vector;
526   };
527
528   auto create_operator = [&fbb, &map,
529                           &variables_to_idx_vector](const TfOpNode &node) {
530     auto &index_map = map.getIndexMap<tflite::BuiltinOperator>();
531
532     auto op_code = index_map.getIndex(node.getOpType());
533     std::vector<int> inputs;
534     if (node.isInputNode()) {
535       inputs = variables_to_idx_vector(node.getInputs());
536     } else {
537       /**
538        *  Q) Why find a tensor that shares a buffer with input tensor?
539        *
540        *  A) the tflite needs only one tensor between nodes. Therefore,
541        *basically, output tensors are used for tflite tensor that shares its
542        *buffer with input's
543        **/
544       TfOpNode::Variables input_tensors;
545       for (auto parent_node : getPredNodes(node)) {
546         for (auto parent_out : parent_node->getOutputs()) {
547           for (auto in : node.getInputs()) {
548             /// second condition is a workaround
549             /// Transpose op output tensor originally had nullptr data pointer
550             /// but it has been allocated (parent_out->getData()). But, the
551             /// buffer that shared its buffer hasn't so it has still nullptr
552             /// (in->getData()).
553             /// @todo remove this workaround
554             if (parent_out->getData() == in->getData() ||
555                 (in->getData() == nullptr && parent_out->getData())) {
556               if (std::find(input_tensors.begin(), input_tensors.end(),
557                             parent_out) != input_tensors.end())
558                 continue;
559               input_tensors.push_back(parent_out);
560             }
561           }
562         }
563       }
564       inputs = variables_to_idx_vector(input_tensors);
565     }
566     auto weights = variables_to_idx_vector(node.getWeights());
567
568     /// weights are part of input in tflite
569     inputs.insert(inputs.end(), weights.begin(), weights.end());
570
571     auto outputs = variables_to_idx_vector(node.getOutputs());
572
573     auto fb_inputs = fbb.CreateVector(inputs);
574     auto fb_outputs = fbb.CreateVector(outputs);
575     auto fb_options = node.getBuiltinOps();
576
577     tflite::OperatorBuilder builder(fbb);
578     builder.add_opcode_index(op_code);
579     builder.add_builtin_options_type(node.getOptionType());
580     builder.add_builtin_options(fb_options);
581     builder.add_inputs(fb_inputs);
582     builder.add_outputs(fb_outputs);
583     return builder.Finish();
584   };
585
586   std::vector<flatbuffers::Offset<tflite::Operator>> v;
587   v.reserve(nodes.size());
588
589   for (auto &node : nodes) {
590     if (node->isVirtualNode())
591       continue;
592     auto op = create_operator(*node);
593     v.push_back(op);
594   }
595
596   return fbb.CreateVector(v);
597 }
598
599 flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<tflite::SubGraph>>>
600 buildSubGraphs(const TfOpNodes &nodes, const TfOpIdxMap &map,
601                flatbuffers::FlatBufferBuilder &fbb) {
602
603   auto tensors = buildTensors(map, fbb);
604   auto ops = buildOperators(nodes, map, fbb);
605
606   /// @todo extract this to buildSubgraph if there is one or more subgraph
607   auto name = fbb.CreateString("main");
608   auto inputs = fbb.CreateVector(map.getInputs());
609   auto outputs = fbb.CreateVector(map.getOutputs());
610
611   auto builder = tflite::SubGraphBuilder(fbb);
612   builder.add_tensors(tensors);
613   builder.add_inputs(inputs);
614   builder.add_outputs(outputs);
615   builder.add_name(name);
616   builder.add_operators(ops);
617   auto subgraph = builder.Finish();
618
619   std::vector<flatbuffers::Offset<tflite::SubGraph>> subgraphs;
620   subgraphs.reserve(1);
621   subgraphs.push_back(subgraph);
622
623   return fbb.CreateVector(subgraphs);
624 }
625
626 } // namespace
627
628 void TfliteInterpreter::serialize(const GraphRepresentation &representation,
629                                   const std::string &out) {
630   /// @todo check if graph is finalized & initialized and ready to serialize.
631
632   /// 0. remove batch normalization layer in GraphRepresentation
633   BnRealizer realizer({});
634   GraphRepresentation graph = realizer.realize(representation);
635
636   /// 1. remove loss layer in GraphRepresentation
637   LossRealizer loss_realizer({});
638   graph = loss_realizer.realize(graph);
639
640   /// 2. The graph must have weights, input dims, output dims set
641   flatbuffers::FlatBufferBuilder fbb;
642
643   auto opNodes = buildOpNodes(graph, fbb);
644   TfOpIdxMap map(opNodes); /// build TfOpIdxMap from opNodes
645
646   auto opcodes = buildOperatorCodes(map, fbb);
647   auto subgraphs = buildSubGraphs(opNodes, map, fbb);
648   auto buffers = buildBuffers(map, fbb);
649   auto desc = fbb.CreateString("This file is generated from NNTrainer");
650
651   tflite::ModelBuilder model_builder(fbb);
652
653   model_builder.add_operator_codes(opcodes);
654   model_builder.add_subgraphs(subgraphs);
655   model_builder.add_buffers(buffers);
656   model_builder.add_version(3);
657   model_builder.add_description(desc);
658   auto model = model_builder.Finish();
659
660   fbb.Finish(model, tflite::ModelIdentifier());
661   builder2file(fbb, out);
662 }
663
664 GraphRepresentation TfliteInterpreter::deserialize(const std::string &in) {
665   /// ======== list of things to consider ========
666   /// we need to reconstruct some properties from the shape
667   /// eg) units are not saved as a property
668
669   /** NYI! */
670   return {};
671 }
672
673 } // namespace nntrainer