Publishing 2019 R1 content
[platform/upstream/dldt.git] / model-optimizer / mo / back / ie_ir_ver_2 / emitter.py
1 """
2  Copyright (c) 2018-2019 Intel Corporation
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 import hashlib
18 from defusedxml.minidom import parseString
19 from xml.etree.ElementTree import Element, SubElement, tostring
20
21 from mo.graph.graph import *
22 from mo.utils.unsupported_ops import UnsupportedOps
23 from mo.utils.utils import refer_to_faq_msg
24 from mo.utils.version import get_version
25
26
27 def serialize_constants(graph: Graph, bin_file_name:str, data_type=np.float32):
28     """
29     Found all data constants that has output edges with 'bin' attribute.
30     Serialize content for such constants to a binary file with name bin_file_name in
31     raw format. Save offset and length of serialized area in the file as 'offset' and 'size'
32     attributes of data node.
33
34     Args:
35         @graph: input graph with op and data nodes
36         @bin_file_name: path to file to write blobs to
37         @data_type: numpy data type to convert all blob elemnts to
38
39     """
40     bin_hashes = {}
41     with open(bin_file_name, 'wb') as bin_file:
42         serialize_constants_recursively(graph, bin_file, data_type, bin_hashes)
43
44
45 def serialize_constants_recursively(graph: Graph, bin_file, data_type, bin_hashes):
46     nodes = sorted(graph.nodes())
47     for node in nodes:
48         node = Node(graph, node)
49
50         if node.kind == 'data' and node.value is not None and any('bin' in d for u, v, d in graph.out_edges(node.node, data=True)):
51             blob = node.value
52             blob_hash = hashlib.sha512(blob.tobytes()).hexdigest()
53
54             if blob_hash in bin_hashes and np.array_equal(blob, bin_hashes[blob_hash]['blob']):
55                 graph.node[node.node]['offset'] = bin_hashes[blob_hash]['offset']
56                 graph.node[node.node]['size'] = bin_hashes[blob_hash]['size']
57             else:
58                 start = bin_file.tell()
59                 blob.tofile(bin_file)
60                 end = bin_file.tell()
61
62                 graph.node[node.node]['offset'] = start
63                 graph.node[node.node]['size'] = end - start
64
65                 bin_hashes[blob_hash] = {'offset': graph.node[node.node]['offset'],
66                                          'size': graph.node[node.node]['size'], 'blob': blob}
67
68                 assert (blob.dtype.itemsize * np.prod(node.shape) == end - start)
69
70             log.debug(
71                 "Detected binary for graph: '{}', node: '{}', id: {}, shape: '{}', offset: '{}', size: '{}'".format(
72                     graph, node.soft_get('name'), node.id, node.shape, node.offset, node.size))
73
74     # separate loop for sub-graph to dump them after all blobs for more natural blob offset ordering
75     # TODO: implement strict order for all blobs in entier IR
76     for node in nodes:
77         node = Node(graph, node)
78         # Dump blobs recursively if sub-graphs are present in the node
79         if node.has_valid('sub_graphs'):
80             for sub_graph_attr_name in node.sub_graphs:
81                 sub_graph = node[sub_graph_attr_name]
82                 serialize_constants_recursively(sub_graph, bin_file, data_type, bin_hashes)
83
84
85 def serialize_mean_image(bin_file_name: str, mean_data=[]):
86     with open(bin_file_name, 'ab') as bin_file:
87         mean_offset = []
88         mean_size = []
89         for x in range(len(mean_data)):
90             start = bin_file.tell()
91             bin_file.write(mean_data[x][:])
92             end = bin_file.tell()
93             mean_offset.append(start)
94             mean_size.append(end - start)
95
96         return mean_offset, mean_size
97
98
99 def xml_shape(shape: np.ndarray, element: Element):
100     for d in shape:
101         dim = SubElement(element, 'dim')
102         if d <= 0:
103             raise Error('The value "{}" for shape is less or equal to 0. May be the input shape of the topology is '
104                         'wrong.'.format(d))
105         if int(d) != d:
106             raise Error('The value "{}" for shape is not integer.'.format(d))
107         if not isinstance(d, np.int64):
108             log.warning('The element of shape is not np.int64 value. Converting the value "{}" to integer'.format(d))
109             d = int(d)
110         dim.text = str(d)
111
112
113 def xml_ports(node: Node, element: Element, edges: Element):
114     # input ports
115     inputs = None  # will create input section only if at least one input is available
116     for u, d in node.get_sorted_inputs():
117         if 'bin' not in d and ('xml_skip' not in d or not d['xml_skip']):
118             if inputs is None:
119                 inputs = SubElement(element, 'input')
120             port = SubElement(inputs, 'port')
121             port.set('id', str(d['in']))
122             assert node.graph.node[u]['shape'] is not None, 'Input shape is not calculated properly for node {}'.format(
123                 node.id)
124             xml_shape(node.graph.node[u]['shape'], port)
125             # u is a data node that has a single producer, let's find it
126             assert (node.graph.node[u]['kind'] == 'data')
127             in_nodes = list(node.graph.in_edges(u, data=True))
128             assert (len(in_nodes) <= 1)
129             if len(in_nodes) == 1:
130                 src, _, out_attrs = in_nodes[0]
131                 edge = SubElement(edges, 'edge')
132                 edge.set('from-layer', str(src))
133                 edge.set('from-port', str(out_attrs['out']))
134                 edge.set('to-layer', str(node.node))
135                 edge.set('to-port', str(d['in']))
136
137     # output ports
138     outputs = None
139     for v, d in node.get_sorted_outputs():
140         if 'xml_skip' not in d or not d['xml_skip']:
141             if outputs is None:
142                 outputs = SubElement(element, 'output')
143             port = SubElement(outputs, 'port')
144             port.set('id', str(d['out']))
145             assert node.graph.node[v][
146                        'shape'] is not None, 'Output shape is not calculated properly for node {}'.format(
147                 node.id)
148             xml_shape(node.graph.node[v]['shape'], port)
149
150
151 def xml_consts(graph: Graph, node: Node, element: Element):
152     blobs = None  # sub-element that will be created on-demand
153     for u, d in node.get_sorted_inputs():
154         if 'bin' in d:
155             if not blobs:
156                 blobs = SubElement(element, 'blobs')
157             const = SubElement(blobs, d['bin'])
158             try:
159                 const.set('offset', str(graph.node[u]['offset']))
160                 const.set('size', str(graph.node[u]['size']))
161             except Exception as e:
162                 raise Error('Unable to access binary attributes ("offset" and/or "size") '
163                     'for blobs for node {}. Details: {}'.format(node.soft_get('name'), e))
164
165
166 def soft_get(node, attr):
167     ''' If node has soft_get callable member, returns node.soft_get(attr), else return <SUB-ELEMENT> '''
168     return node.soft_get(attr) if hasattr(node, 'soft_get') and callable(node.soft_get) else '<SUB-ELEMENT>'
169
170
171 def serialize_element(
172         graph: Graph,
173         node,
174         schema: list,
175         parent_element: Element,
176         edges: Element,
177         unsupported):
178
179     name, attrs, subelements = schema
180     element = SubElement(parent_element, name)
181     for attr in attrs:
182         if isinstance(attr, tuple):
183             key = attr[0]
184             try:
185                 if callable(attr[1]):
186                     value = attr[1](node)
187                 else:
188                     value = node[attr[1]] if attr[1] in node else None
189             except TypeError as e:
190                 raise Error('Unable to extract {} from layer {}', key, soft_get(node, 'name')) from e
191             except Exception as e:
192                 raise Error(
193                     'Cannot emit value for attribute {} for layer {}. '
194                     'Internal attribute template: {}.',
195                     key,
196                     soft_get(node, 'name'),
197                     attr
198                 ) from e
199         elif isinstance(attr, dict):
200             node_attrs = node.graph.node[node.id] if isinstance(node, Node) else node
201             for key in attr.keys():
202                 if key in node_attrs:
203                     for k, v in node_attrs[key].items():
204                         element.set(k, str(v))
205             continue
206         else:
207             key = attr
208             value = node[attr] if attr in node else None
209         if value is not None:
210             element.set(key, str(value))
211     serialize_node_attributes(graph, node, subelements, element, edges, unsupported)
212     if len(element.attrib) == 0 and len(element.getchildren()) == 0:
213         parent_element.remove(element)
214
215
216 def serialize_meta_list(graph, node, schema, element, edges, unsupported):
217     _, list_accessor, sub_schema = schema
218     items = list_accessor(node)  # this is a list of dictionary-like objects
219     for item in items:
220         serialize_node_attributes(graph, item, [sub_schema], element, edges, unsupported)
221
222
223 def serialize_node_attributes(
224         graph: Graph,  # the current network graph
225         node,   # dictionry-like object that should be serialized
226         schema: list,
227         parent_element: Element,
228         edges: Element,
229         unsupported):
230
231     try:
232         for s in schema:
233             if not isinstance(s, tuple):
234                 if s == '@ports':
235                     try:
236                         # TODO make sure that edges are generated regardless of the existence of @ports
237                         xml_ports(node, parent_element, edges)
238                     except Exception as e:
239                         raise Error(('Unable to create ports for node with id {}. ' +
240                                      refer_to_faq_msg(3)).format(node.id)) from e
241                 elif s == '@consts':
242                     xml_consts(graph, node, parent_element)
243                 else:
244                     log.warning('Unknown xml schema tag: {}'.format(s))
245             else:
246                 name = s[0]
247                 if name == '@list':
248                     serialize_meta_list(graph, node, s, parent_element, edges, unsupported)
249                 elif name == '@network':
250                     serialize_network(node[s[1]], parent_element, unsupported)
251                 else:
252                     serialize_element(graph, node, s, parent_element, edges, unsupported)
253     except Exception as e:
254         raise Error(
255             'Error while emitting attributes for layer {} (id = {}). '
256             'It usually means that there is unsupported pattern around this node or unsupported combination of attributes.',
257             soft_get(node, 'name'),
258             node.id
259         ) from e
260
261
262 def create_pre_process_block_for_image(net: Element, ref_layer_names: list, mean_offset: tuple,
263                                        mean_size: tuple):
264     pre_process = SubElement(net, 'pre-process')
265     pre_process.set('mean-precision', 'FP32')  # TODO: to think about need to output FP16 mean values
266     # TODO: extend it for several inputs
267     pre_process.set('reference-layer-name', ref_layer_names[0])
268     for idx in range(len(mean_size)):
269         channel_xml = SubElement(pre_process, 'channel')
270         channel_xml.set('id', str(idx))
271         mean_xml = SubElement(channel_xml, 'mean')
272         mean_xml.set('offset', str(mean_offset[idx]))
273         mean_xml.set('size', str(mean_size[idx]))
274
275
276 def create_pre_process_block(net, ref_layer_name, means, scales=None):
277     """
278     Generates the pre-process block for the IR XML
279     Args:
280         net: root XML element
281         ref_layer_name: name of the layer where it is referenced to
282         means: tuple of values
283         scales: tuple of values
284
285     Returns:
286         pre-process XML element
287     """
288     pre_process = SubElement(net, 'pre-process')
289     pre_process.set('reference-layer-name', ref_layer_name)
290
291     for idx in range(len(means)):
292         channel_xml = SubElement(pre_process, 'channel')
293         channel_xml.set('id', str(idx))
294
295         mean_xml = SubElement(channel_xml, 'mean')
296         mean_xml.set('value', str(means[idx]))
297
298         if scales:
299             scale_xml = SubElement(channel_xml, 'scale')
300             scale_xml.set('value', str(scales[idx]))
301
302     return pre_process
303
304
305 def add_quantization_statistics(graph, net_element):
306     if 'statistics' in graph.graph:
307         stats = SubElement(net_element, 'statistics')
308         for tensor, interval in graph.graph['statistics'].items():
309             layer = SubElement(stats, 'layer')
310             name = SubElement(layer, 'name')
311             name.text = tensor
312             min = SubElement(layer, 'min')
313             min.text = interval['min']
314             max = SubElement(layer, 'max')
315             max.text = interval['max']
316         log.info('Statistics were inserted to IR')
317
318
319 def add_meta_data(net: Element, meta_info: dict):
320     meta = SubElement(net, 'meta_data')
321     SubElement(meta, 'MO_version').set('value', get_version())
322     parameters = SubElement(meta, 'cli_parameters')
323     [SubElement(parameters, str(key)).set('value', str(meta_info[key])) for key in sorted(meta_info.keys()) if
324      key != 'unset']
325     SubElement(parameters, 'unset').set('unset_cli_parameters', ', '.join(sorted(meta_info['unset'])))
326
327
328 def serialize_network(graph, net_element, unsupported):
329     layers = SubElement(net_element, 'layers')
330     edges = SubElement(net_element, 'edges')
331     if graph is None:
332         return
333     nodes = sorted(graph.nodes())
334     for node in nodes:
335         node = Node(graph, node)
336         if not node.has('IE'):
337             continue
338         if node.kind == 'op' and (not node.has('type') or node.type is None):
339             unsupported.add(node)
340             continue
341         try:
342             serialize_node_attributes(graph, node, node.IE, layers, edges, unsupported)
343         except Error as e:
344             raise Error(str(e).replace('<SUB-ELEMENT>', '{} (id = {})'.format(node.soft_get('name'), node.id))) from e
345
346
347 def generate_ie_ir(graph: Graph, file_name: str, input_names: tuple = (), mean_offset: tuple = (),
348                    mean_size: tuple = (), meta_info: dict = dict()):
349     """
350     Extracts IE/IR attributes from kind='op' nodes in three ways:
351       (1) node.IE xml scheme that set correspondance from existing attributes to generated xml elements
352       (2) input/output edges that don't have 'bin' attributes are transformed to input/output ports
353       (3) input edges that has 'bin' attributes are handled in special way like weights/biases
354
355     Args:
356         graph: nx graph with FW-independent model
357         file_name: name of the resulting IR
358         input_names: names of input layers of the topology to add mean file to
359         input_name: name of the layer which is referenced from pre-processing block if any
360         mean_values: tuple of mean values for channels in RGB order
361         scale_values:  tuple of mean values for channels in RGB order
362         mean_offset: offset in binary file, where mean file values start
363         mean_size: size of the mean file
364     """
365     net = Element('net')
366     net.set('name', graph.name)
367     net.set('version', str((graph.graph['ir_version'])))
368     net.set('batch', '1')  # TODO substitute real batches here (is it a number or is it an index?)
369
370     if mean_size or mean_offset:
371         create_pre_process_block_for_image(net, input_names, mean_offset, mean_size)
372
373     if 'mean_values' in graph.graph.keys():
374         for input_name, values in graph.graph['mean_values'].items():
375             create_pre_process_block(net, input_name, values)
376
377     unsupported = UnsupportedOps(graph)
378
379     serialize_network(graph, net, unsupported)
380     add_quantization_statistics(graph, net)
381     add_meta_data(net, meta_info)
382     xml_string = tostring(net)
383     xml_doc = parseString(xml_string)
384     pretty_xml_as_string = xml_doc.toprettyxml()
385     if len(unsupported.unsupported):
386         log.debug('Partially correct IR XML:\n{}'.format(pretty_xml_as_string))
387         unsupported.report(log.error, "List of operations that cannot be converted to Inference Engine IR:")
388         raise Error('Part of the nodes was not converted to IR. Stopped. ' +
389                     refer_to_faq_msg(24))
390     with open(file_name, 'w') as file:
391         file.write(pretty_xml_as_string)
392
393
394 def port_renumber(graph: Graph):
395     for node in list(graph.nodes()):
396         node = Node(graph, node)
397         if node.kind == 'op':
398             base = 0
399             for u, d in node.get_sorted_inputs():
400                 d['in'] = base
401                 base += 1
402             for v, d in node.get_sorted_outputs():
403                 d['out'] = base
404                 base += 1