Imported Upstream version 1.19.0
[platform/core/ml/nnfw.git] / compiler / one-cmds / utils.py
1 #!/usr/bin/env python
2
3 # Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 #    http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 import argparse
18 import configparser
19 import glob
20 import ntpath
21 import os
22 import subprocess
23 import sys
24
25
26 class _CONSTANT:
27     __slots__ = ()  # This prevents access via __dict__.
28     OPTIMIZATION_OPTS = (
29         # (OPTION_NAME, HELP_MESSAGE)
30         ('O1', 'enable O1 optimization pass'),
31         ('convert_nchw_to_nhwc',
32          'Experimental: This will convert NCHW operators to NHWC under the assumption that input model is NCHW.'
33          ),
34         ('expand_broadcast_const', 'expand broadcastable constant node inputs'),
35         ('nchw_to_nhwc_input_shape',
36          'convert the input shape of the model (argument for convert_nchw_to_nhwc)'),
37         ('nchw_to_nhwc_output_shape',
38          'convert the output shape of the model (argument for convert_nchw_to_nhwc)'),
39         ('fold_add_v2', 'fold AddV2 op with constant inputs'),
40         ('fold_cast', 'fold Cast op with constant input'),
41         ('fold_dequantize', 'fold Dequantize op'),
42         ('fold_dwconv', 'fold Depthwise Convolution op with constant inputs'),
43         ('fold_sparse_to_dense', 'fold SparseToDense op'),
44         ('forward_reshape_to_unaryop', 'Forward Reshape op'),
45         ('fuse_add_with_tconv', 'fuse Add op to Transposed'),
46         ('fuse_add_with_fully_connected', 'fuse Add op to FullyConnected op'),
47         ('fuse_batchnorm_with_conv', 'fuse BatchNorm op to Convolution op'),
48         ('fuse_batchnorm_with_dwconv', 'fuse BatchNorm op to Depthwise Convolution op'),
49         ('fuse_batchnorm_with_tconv', 'fuse BatchNorm op to Transposed Convolution op'),
50         ('fuse_bcq', 'apply Binary Coded Quantization'),
51         ('fuse_preactivation_batchnorm',
52          'fuse BatchNorm operators of pre-activations to Convolution op'),
53         ('fuse_mean_with_mean', 'fuse two consecutive Mean ops'),
54         ('fuse_transpose_with_mean',
55          'fuse Mean with a preceding Transpose under certain conditions'),
56         ('make_batchnorm_gamma_positive',
57          'make negative gamma of BatchNorm to a small positive value (1e-10).'
58          ' Note that this pass can change the execution result of the model.'
59          ' So, use it only when the impact is known to be acceptable.'),
60         ('fuse_activation_function', 'fuse Activation function to a preceding operator'),
61         ('fuse_instnorm', 'fuse ops to InstanceNorm operator'),
62         ('replace_cw_mul_add_with_depthwise_conv',
63          'replace channel-wise Mul/Add with DepthwiseConv2D'),
64         ('remove_fakequant', 'remove FakeQuant ops'),
65         ('remove_quantdequant', 'remove Quantize-Dequantize sequence'),
66         ('remove_redundant_reshape', 'fuse or remove subsequent Reshape ops'),
67         ('remove_redundant_transpose', 'fuse or remove subsequent Transpose ops'),
68         ('remove_unnecessary_reshape', 'remove unnecessary reshape ops'),
69         ('remove_unnecessary_slice', 'remove unnecessary slice ops'),
70         ('remove_unnecessary_strided_slice', 'remove unnecessary strided slice ops'),
71         ('remove_unnecessary_split', 'remove unnecessary split ops'),
72         ('resolve_customop_add', 'convert Custom(Add) op to Add op'),
73         ('resolve_customop_batchmatmul',
74          'convert Custom(BatchMatmul) op to BatchMatmul op'),
75         ('resolve_customop_matmul', 'convert Custom(Matmul) op to Matmul op'),
76         ('resolve_customop_max_pool_with_argmax',
77          'convert Custom(MaxPoolWithArgmax) to net of builtin operators'),
78         ('shuffle_weight_to_16x1float32',
79          'convert weight format of FullyConnected op to SHUFFLED16x1FLOAT32.'
80          ' Note that it only converts weights whose row is a multiple of 16'),
81         ('substitute_pack_to_reshape', 'convert single input Pack op to Reshape op'),
82         ('substitute_padv2_to_pad', 'convert certain condition PadV2 to Pad'),
83         ('substitute_splitv_to_split', 'convert certain condition SplitV to Split'),
84         ('substitute_squeeze_to_reshape', 'convert certain condition Squeeze to Reshape'),
85         ('substitute_strided_slice_to_reshape',
86          'convert certain condition StridedSlice to Reshape'),
87         ('substitute_transpose_to_reshape',
88          'convert certain condition Transpose to Reshape'),
89         ('transform_min_max_to_relu6', 'transform Minimum-Maximum pattern to Relu6 op'),
90         ('transform_min_relu_to_relu6', 'transform Minimum(6)-Relu pattern to Relu6 op'))
91
92
93 _CONSTANT = _CONSTANT()
94
95
96 def _add_default_arg(parser):
97     # version
98     parser.add_argument(
99         '-v',
100         '--version',
101         action='store_true',
102         help='show program\'s version number and exit')
103
104     # verbose
105     parser.add_argument(
106         '-V',
107         '--verbose',
108         action='store_true',
109         help='output additional information to stdout or stderr')
110
111     # configuration file
112     parser.add_argument('-C', '--config', type=str, help='run with configuation file')
113     # section name that you want to run in configuration file
114     parser.add_argument('-S', '--section', type=str, help=argparse.SUPPRESS)
115
116
117 def is_accumulated_arg(arg, driver):
118     if driver == "one-quantize":
119         if arg == "tensor_name" or arg == "scale" or arg == "zero_point":
120             return True
121
122     return False
123
124
125 def _is_valid_attr(args, attr):
126     return hasattr(args, attr) and getattr(args, attr)
127
128
129 def _parse_cfg_and_overwrite(config_path, section, args):
130     """
131     parse given section of configuration file and set the values of args.
132     Even if the values parsed from the configuration file already exist in args,
133     the values are overwritten.
134     """
135     if config_path == None:
136         # DO NOTHING
137         return
138     config = configparser.ConfigParser()
139     # make option names case sensitive
140     config.optionxform = str
141     parsed = config.read(config_path)
142     if not parsed:
143         raise FileNotFoundError('Not found given configuration file')
144     if not config.has_section(section):
145         raise AssertionError('configuration file doesn\'t have \'' + section +
146                              '\' section')
147     for key in config[section]:
148         setattr(args, key, config[section][key])
149     # TODO support accumulated arguments
150
151
152 def _parse_cfg(args, driver_name):
153     """parse configuration file. If the option is directly given to the command line,
154        the option is processed prior to the configuration file.
155        That is, if the values parsed from the configuration file already exist in args,
156        the values are ignored."""
157     if _is_valid_attr(args, 'config'):
158         config = configparser.ConfigParser()
159         config.optionxform = str
160         config.read(args.config)
161         # if section is given, verify given section
162         if _is_valid_attr(args, 'section'):
163             if not config.has_section(args.section):
164                 raise AssertionError('configuration file must have \'' + driver_name +
165                                      '\' section')
166             for key in config[args.section]:
167                 if is_accumulated_arg(key, driver_name):
168                     if not _is_valid_attr(args, key):
169                         setattr(args, key, [config[args.section][key]])
170                     else:
171                         getattr(args, key).append(config[args.section][key])
172                     continue
173                 if not _is_valid_attr(args, key):
174                     setattr(args, key, config[args.section][key])
175         # if section is not given, section name is same with its driver name
176         else:
177             if not config.has_section(driver_name):
178                 raise AssertionError('configuration file must have \'' + driver_name +
179                                      '\' section')
180             secton_to_run = driver_name
181             for key in config[secton_to_run]:
182                 if is_accumulated_arg(key, driver_name):
183                     if not _is_valid_attr(args, key):
184                         setattr(args, key, [config[secton_to_run][key]])
185                     else:
186                         getattr(args, key).append(config[secton_to_run][key])
187                     continue
188                 if not _is_valid_attr(args, key):
189                     setattr(args, key, config[secton_to_run][key])
190
191
192 def _make_tf2tfliteV2_cmd(args, driver_path, input_path, output_path):
193     """make a command for running tf2tfliteV2.py"""
194     cmd = [sys.executable, os.path.expanduser(driver_path)]
195     # verbose
196     if _is_valid_attr(args, 'verbose'):
197         cmd.append('--verbose')
198     # model_format
199     if _is_valid_attr(args, 'model_format_cmd'):
200         cmd.append(getattr(args, 'model_format_cmd'))
201     elif _is_valid_attr(args, 'model_format'):
202         cmd.append('--' + getattr(args, 'model_format'))
203     else:
204         cmd.append('--graph_def')  # default value
205     # converter version
206     if _is_valid_attr(args, 'converter_version_cmd'):
207         cmd.append(getattr(args, 'converter_version_cmd'))
208     elif _is_valid_attr(args, 'converter_version'):
209         cmd.append('--' + getattr(args, 'converter_version'))
210     else:
211         cmd.append('--v1')  # default value
212     # input_path
213     if _is_valid_attr(args, 'input_path'):
214         cmd.append('--input_path')
215         cmd.append(os.path.expanduser(input_path))
216     # output_path
217     if _is_valid_attr(args, 'output_path'):
218         cmd.append('--output_path')
219         cmd.append(os.path.expanduser(output_path))
220     # input_arrays
221     if _is_valid_attr(args, 'input_arrays'):
222         cmd.append('--input_arrays')
223         cmd.append(getattr(args, 'input_arrays'))
224     # input_shapes
225     if _is_valid_attr(args, 'input_shapes'):
226         cmd.append('--input_shapes')
227         cmd.append(getattr(args, 'input_shapes'))
228     # output_arrays
229     if _is_valid_attr(args, 'output_arrays'):
230         cmd.append('--output_arrays')
231         cmd.append(getattr(args, 'output_arrays'))
232
233     return cmd
234
235
236 def _make_tflite2circle_cmd(driver_path, input_path, output_path):
237     """make a command for running tflite2circle"""
238     cmd = [driver_path, input_path, output_path]
239     return [os.path.expanduser(c) for c in cmd]
240
241
242 def _make_circle2circle_cmd(args, driver_path, input_path, output_path):
243     """make a command for running circle2circle"""
244     cmd = [os.path.expanduser(c) for c in [driver_path, input_path, output_path]]
245     # profiling
246     if _is_valid_attr(args, 'generate_profile_data'):
247         cmd.append('--generate_profile_data')
248     # optimization pass(only true/false options)
249     # TODO support options whose number of arguments is more than zero
250     for opt in _CONSTANT.OPTIMIZATION_OPTS:
251         if _is_valid_attr(args, opt[0]):
252             # ./driver --opt[0]
253             if type(getattr(args, opt[0])) is bool:
254                 cmd.append('--' + opt[0])
255             """
256             This condition check is for config file interface, usually would be
257              SomeOption=True
258             but user can write as follows while development
259              SomeOption=False
260             instead of removing SomeOption option
261             """
262             if type(getattr(args, opt[0])) is str and not getattr(
263                     args, opt[0]).lower() in ['false', '0', 'n']:
264                 cmd.append('--' + opt[0])
265
266     return cmd
267
268
269 def _print_version_and_exit(file_path):
270     """print version of the file located in the file_path"""
271     script_path = os.path.realpath(file_path)
272     dir_path = os.path.dirname(script_path)
273     script_name = os.path.splitext(os.path.basename(script_path))[0]
274     # run one-version
275     subprocess.call([os.path.join(dir_path, 'one-version'), script_name])
276     sys.exit()
277
278
279 def _safemain(main, mainpath):
280     """execute given method and print with program name for all uncaught exceptions"""
281     try:
282         main()
283     except Exception as e:
284         prog_name = os.path.basename(mainpath)
285         print(f"{prog_name}: {type(e).__name__}: " + str(e), file=sys.stderr)
286         sys.exit(255)
287
288
289 def _run(cmd, err_prefix=None, logfile=None):
290     """Execute command in subprocess
291
292     Args:
293         cmd: command to be executed in subprocess
294         err_prefix: prefix to be put before every stderr lines
295         logfile: file stream to which both of stdout and stderr lines will be written
296     """
297     with subprocess.Popen(
298             cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1) as p:
299         import select
300         inputs = set([p.stdout, p.stderr])
301         while inputs:
302             readable, _, _ = select.select(inputs, [], [])
303             for x in readable:
304                 line = x.readline()
305                 if len(line) == 0:
306                     inputs.discard(x)
307                     continue
308                 if x == p.stdout:
309                     out = sys.stdout
310                 if x == p.stderr:
311                     out = sys.stderr
312                     if err_prefix:
313                         line = f"{err_prefix}: ".encode() + line
314                 out.buffer.write(line)
315                 out.buffer.flush()
316                 if logfile != None:
317                     logfile.write(line)
318     if p.returncode != 0:
319         sys.exit(p.returncode)
320
321
322 def _remove_prefix(str, prefix):
323     if str.startswith(prefix):
324         return str[len(prefix):]
325     return str
326
327
328 def _remove_suffix(str, suffix):
329     if str.endswith(suffix):
330         return str[:-len(suffix)]
331     return str
332
333
334 def _get_optimization_list(get_name=False):
335     """
336     returns a list of optimization. If `get_name` is True,
337     only basename without extension is returned rather than full file path.
338
339     [one hierarchy]
340     one
341     ├── backends
342     ├── bin
343     ├── doc
344     ├── include
345     ├── lib
346     ├── optimization
347     └── test
348
349     Optimization options must be placed in `optimization` folder
350     """
351     dir_path = os.path.dirname(os.path.realpath(__file__))
352
353     # optimization folder
354     files = [f for f in glob.glob(dir_path + '/../optimization/O*.cfg', recursive=True)]
355     # exclude if the name has space
356     files = [s for s in files if not ' ' in s]
357
358     opt_list = []
359     for cand in files:
360         base = ntpath.basename(cand)
361         if os.path.isfile(cand) and os.access(cand, os.R_OK):
362             opt_list.append(cand)
363
364     if get_name == True:
365         # NOTE the name includes prefix 'O'
366         # e.g. O1, O2, ONCHW not just 1, 2, NCHW
367         opt_list = [ntpath.basename(f) for f in opt_list]
368         opt_list = [_remove_suffix(s, '.cfg') for s in opt_list]
369
370     return opt_list