02f63abed844cf11b8200b76ef565c3071d4f974
[platform/core/ml/nnfw.git] / compiler / visq / visq
1 #!/usr/bin/env bash
2 ''''export SCRIPT_PATH="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" # '''
3 ''''export PY_PATH=${SCRIPT_PATH}/venv/bin/python                                       # '''
4 ''''test -f ${PY_PATH} && exec ${PY_PATH} "$0" "$@"                                     # '''
5 ''''echo "Error: Virtual environment not found. Please run 'one-prepare-venv' command." # '''
6 ''''exit 255                                                                            # '''
7
8 # Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved
9 #
10 # Licensed under the Apache License, Version 2.0 (the "License");
11 # you may not use this file except in compliance with the License.
12 # You may obtain a copy of the License at
13 #
14 #    http://www.apache.org/licenses/LICENSE-2.0
15 #
16 # Unless required by applicable law or agreed to in writing, software
17 # distributed under the License is distributed on an "AS IS" BASIS,
18 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 # See the License for the specific language governing permissions and
20 # limitations under the License.
21
22 import argparse
23 import subprocess
24 import tempfile
25 import json
26 import os
27 import math
28 import sys
29
30 import h5py as h5
31 import numpy as np
32
33 from shutil import copyfile
34 from pathlib import Path
35
36 from visqlib.Palette import YLORRD9Palette
37 from visqlib.QErrorComputer import MPEIRComputer, MSEComputer, TAEComputer
38 from visqlib.Util import valid_attr, pretty_float
39 from visqlib.DotBuilder import DotBuilder
40
41
42 def _get_parser():
43     parser = argparse.ArgumentParser(
44         description='Command line tool to visualize layer-wise quantization errors')
45     parser.add_argument(
46         "-f",
47         "--fp32_circle",
48         type=str,
49         help="Path to the fp32 circle model.",
50         required=True)
51     parser.add_argument(
52         "-q",
53         "--q_circle",
54         type=str,
55         help="Path to the quantized circle model.",
56         required=True)
57     parser.add_argument(
58         "-d",
59         "--data",
60         type=str,
61         help=
62         "Path to the data used for inference. Random data will be used if this option is not given.",
63         required=False)
64     parser.add_argument(
65         "--mpeir_output",
66         type=str,
67         help="Path to the output json file (qerror metric = MPEIR).",
68         required=False)
69     parser.add_argument(
70         "--mse_output",
71         type=str,
72         help="Path to the output json file (qerror metric = MSE).",
73         required=False)
74     parser.add_argument(
75         "--tae_output",
76         type=str,
77         help="Path to the output json file (qerror metric = TAE).",
78         required=False)
79     parser.add_argument(
80         "--dump_dot_graph", action="store_true", help="Dump dot graph.", required=False)
81     parser.add_argument(
82         "-b",
83         "--batch_size",
84         type=int,
85         help="Batch size to process large datasets.",
86         required=False)
87
88     return parser
89
90
91 def _verify_args(args):
92     """Verify the given arguments"""
93
94     valid_outputs = ['mpeir_output', 'mse_output', 'tae_output']
95
96     # Check if at least one output option is given
97     num_outputs = 0
98     for output_name in valid_outputs:
99         if valid_attr(args, output_name):
100             num_outputs += 1
101
102     if num_outputs == 0:
103         raise RuntimeError("At least one output should be given.")
104
105
106 def _run_dalgona(model, data, analysis, save_dir):
107     dir_path = Path(__file__).parent.resolve()
108     dalgona_path = os.path.join(dir_path, 'dalgona')
109     cmd = [dalgona_path]
110     cmd += ['--input_model', str(model)]
111     cmd += ['--analysis', str(analysis)]
112     if data != None:
113         cmd += ['--input_data', str(data)]
114     cmd += ['--analysis_args', str(save_dir)]
115
116     try:
117         subprocess.run(cmd, capture_output=True, check=True)
118     except subprocess.CalledProcessError as e:
119         print('Error raised while running the below command')
120         print(' '.join(cmd))
121         print(e.output)
122         raise
123
124
125 # Generate h5 file that contains a dataset of a single batch
126 # This is for batch execution of visq
127 def gen_batch_h5(inputs_data, inputs_path):
128     # Create h5 file
129     output_path = inputs_path + "/inputs.h5"
130     h5_file = h5.File(output_path, 'w')
131     group = h5_file.create_group("value")
132     group.attrs['desc'] = "Input data"
133
134     for i in range(len(inputs_data)):
135         sample = group.create_group(str(i))
136         for j in range(len(inputs_data[i])):
137             sample.create_dataset(str(j), data=inputs_data[i][j])
138
139     h5_file.close()
140     return output_path
141
142
143 # Aggregate intermediate results for a given data
144 def advance_on_data(fp32_model, fq_model, data, computers):
145
146     curr_dir = Path(__file__).parent.resolve()
147     dump_fp32_py = curr_dir / 'visqlib' / 'DumpFP32FM.py'
148     dump_fq_py = curr_dir / 'visqlib' / 'DumpFakeQuantFM.py'
149
150     with tempfile.TemporaryDirectory() as fp32_dir, \
151          tempfile.TemporaryDirectory() as fq_dir:
152
153         _run_dalgona(fp32_model, data, dump_fp32_py, fp32_dir)
154         copyfile(fp32_dir + '/tensors.txt', fq_dir + '/tensors.txt')
155         _run_dalgona(fq_model, data, dump_fq_py, fq_dir)
156
157         for metric_key in computers:
158             computers[metric_key][0].advance_on(fp32_dir, fq_dir)
159
160
161 def _run_batch(fp32_model, fq_model, data, computers, batch_size):
162     with tempfile.TemporaryDirectory() as inputs_dir:
163         with h5.File(data, 'r') as f:
164             dataset = f['value']
165
166             inputs = []
167             for data_index in dataset:
168                 cur_inputs = []
169                 for input_index in dataset[data_index]:
170                     d = dataset[data_index][input_index][:]
171                     cur_inputs.append(np.array(d, np.float32))
172
173                 inputs.append(cur_inputs)
174                 if len(inputs) >= batch_size:
175                     input_path = gen_batch_h5(inputs, inputs_dir)
176                     advance_on_data(fp32_model, fq_model, input_path, computers)
177                     inputs = []
178
179             if len(inputs) > 0:
180                 input_path = gen_batch_h5(inputs, inputs_dir)
181                 advance_on_data(fp32_model, fq_model, input_path, computers)
182
183
184 def _fake_quantize(input_model, output_model):
185     dir_path = Path(__file__).parent.resolve()
186     circle_quantizer_path = os.path.join(dir_path, 'circle-quantizer')
187     cmd = [circle_quantizer_path]
188     cmd += ['--fake_quantize']
189     cmd += [str(input_model)]
190     cmd += [str(output_model)]
191
192     try:
193         subprocess.run(cmd, check=True)
194     except subprocess.CalledProcessError as e:
195         print('Error raised while running the below command')
196         print(' '.join(cmd))
197         print(e.output)
198         raise
199
200
201 # Recursively visit items and check if there is Infinity or NaN
202 def _check_float(item):
203     if isinstance(item, dict):
204         for v in item.values():
205             _check_float(v)
206     if isinstance(item, list):
207         for v in item:
208             _check_float(v)
209     if isinstance(item, float):
210         if item == -float('inf') or item == float('inf'):
211             raise RuntimeError('Infinite value detected. Value must be float')
212         if math.isnan(item):
213             raise RuntimeError('NaN value detected. Value must be float')
214
215
216 def _build_json(model, metric, colorscheme, error):
217     # model: string
218     # metric: string
219     # colorscheme: list ['b': begin, 'e': end, 'c':color]
220     # error: dict {tensor_name:error}
221
222     meta = {}
223     meta["model"] = model
224     meta["metric"] = metric
225     meta["colorscheme"] = pretty_float(colorscheme)
226     result = {}
227     result["meta"] = meta
228     # Why list? To support multiple subgraphs
229     result["error"] = [pretty_float(error)]
230
231     # Invariants
232     _check_float(meta["colorscheme"])
233     _check_float(result["error"])
234     return result
235
236
237 def _save_dot(circle_path: str, dot_path: str, metric: str, colors: list, qerror: dict):
238     # circle_path: Path to the circle model (required to build graph)
239     # dot_path: Path to the output dot file
240     # metric: Metric name (ex: MPEIR, MSE)
241     # colors: list [{'b': begin, 'e': end, 'c':color}, ..]
242     # qerror: dict {tensor_name (str) -> qerror (float)}
243     builder = DotBuilder(
244         circle_path=circle_path, dot_path=dot_path, metric=metric, colors=colors)
245
246     builder.save(qerror)
247
248
249 def run_on_data_batchwise(fp32_model, q_model, data, dump_dot_graph, computers,
250                           batch_size):
251
252     with tempfile.TemporaryDirectory() as model_dir:
253         fq_model = model_dir + '/fq_model.circle'
254
255         # Step 1. Fake quantize quantized circle model
256         _fake_quantize(q_model, fq_model)
257
258         # process the whole dataset batch by batch
259         _run_batch(fp32_model, fq_model, data, computers, batch_size)
260
261         #compute the final results
262         for metric_key in computers:
263             cur_computer = computers[metric_key][0]
264             output = computers[metric_key][1]
265             if metric_key == 'MPEIR':
266                 qerror_map = cur_computer.get_final_result()
267                 q_min = 0.0
268                 q_max = 1.0
269             elif metric_key == 'MSE' or metric_key == 'TAE':
270                 qerror_map, q_min, q_max = cur_computer.get_final_result()
271
272             palette = YLORRD9Palette(qerror_min=q_min, qerror_max=q_max)
273             result = _build_json(
274                 metric=metric_key,
275                 model=Path(fp32_model).name,
276                 colorscheme=palette.colorscheme(),
277                 error=qerror_map)
278             with open(output, "w") as f:
279                 json.dump(result, f)
280
281             if dump_dot_graph:
282                 _save_dot(
283                     circle_path=fp32_model,
284                     dot_path=output + '.dot',
285                     metric=metric_key,
286                     colors=palette.colorscheme(),
287                     qerror=qerror_map)
288
289
290 def run_on_data(fp32_model, q_model, data, dump_dot_graph, computers):
291     curr_dir = Path(__file__).parent.resolve()
292     dump_fp32_py = curr_dir / 'visqlib' / 'DumpFP32FM.py'
293     dump_fq_py = curr_dir / 'visqlib' / 'DumpFakeQuantFM.py'
294
295     with tempfile.TemporaryDirectory() as model_dir, \
296          tempfile.TemporaryDirectory() as fp32_dir, \
297          tempfile.TemporaryDirectory() as fq_dir:
298         fq_model = model_dir + '/fq_model.circle'
299
300         # Step 1. Fake quantize quantized circle model
301         _fake_quantize(q_model, fq_model)
302
303         # Step 2. Run dalgona to dump intermediate FMs in FP32 model
304         _run_dalgona(fp32_model, data, dump_fp32_py, fp32_dir)
305
306         # Copy list of dumped tensors
307         copyfile(fp32_dir + '/tensors.txt', fq_dir + '/tensors.txt')
308
309         # Step 3. Run dalgona to dump intermediate FMs in fq model
310         _run_dalgona(fq_model, data, dump_fq_py, fq_dir)
311
312         # Step 4. Read results and compute qerror
313         for metric_key in computers:
314             cur_computer = computers[metric_key][0]
315             output = computers[metric_key][1]
316             cur_computer.advance_on(fp32_dir, fq_dir)
317             if metric_key == 'MPEIR':
318                 qerror_map = cur_computer.get_final_result()
319                 q_min = 0.0
320                 q_max = 1.0
321             elif metric_key == 'MSE' or metric_key == 'TAE':
322                 qerror_map, q_min, q_max = cur_computer.get_final_result()
323
324             palette = YLORRD9Palette(qerror_min=q_min, qerror_max=q_max)
325             result = _build_json(
326                 metric=metric_key,
327                 model=Path(fp32_model).name,
328                 colorscheme=palette.colorscheme(),
329                 error=qerror_map)
330             with open(output, "w") as f:
331                 json.dump(result, f)
332
333             if dump_dot_graph:
334                 _save_dot(
335                     circle_path=fp32_model,
336                     dot_path=output + '.dot',
337                     metric=metric_key,
338                     colors=palette.colorscheme(),
339                     qerror=qerror_map)
340
341
342 def main():
343     # parse arguments
344     parser = _get_parser()
345     args = parser.parse_args()
346     _verify_args(args)
347
348     fp32_model = args.fp32_circle
349     q_model = args.q_circle
350     data = None
351     if valid_attr(args, 'data'):
352         data = args.data
353     dump_dot_graph = args.dump_dot_graph
354     batch_size = None
355     if valid_attr(args, 'batch_size'):
356         batch_size = args.batch_size
357
358     computers = {}
359     if args.mpeir_output:
360         computers['MPEIR'] = (MPEIRComputer(None, None), args.mpeir_output)
361
362     if args.mse_output:
363         computers['MSE'] = (MSEComputer(None, None), args.mse_output)
364
365     if args.tae_output:
366         computers['TAE'] = (TAEComputer(None, None), args.tae_output)
367
368     if batch_size == None:
369         run_on_data(fp32_model, q_model, data, dump_dot_graph, computers)
370     else:
371         run_on_data_batchwise(fp32_model, q_model, data, dump_dot_graph, computers,
372                               batch_size)
373
374
375 if __name__ == '__main__':
376     try:
377         main()
378     except Exception as e:
379         prog_name = os.path.basename(__file__)
380         print(f"{prog_name}: {type(e).__name__}: " + str(e), file=sys.stderr)
381         sys.exit(255)