c60556f59b72f26af7ce245438ba84b82055a405
[platform/core/ml/nnfw.git] / compiler / visq / visqlib / QErrorComputer.py
1 # Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #    http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import os
16 import glob
17 import numpy as np
18
19 from pathlib import Path
20 from visqlib.Util import to_filename
21
22
23 class QErrorComputer:
24     def __init__(self, fp32_dir, fq_dir):
25         self._fp32_dir = fp32_dir
26         self._fq_dir = fq_dir
27         self.qerror_map = dict()
28         self._num_processed_data = 0
29
30     def collect_data_path(self, fp32_dir, fq_dir):
31         # Assumption: FM data are saved as follows
32         #
33         # fp32_dir/
34         #   tensors.txt
35         #   <DATA_INDEX>/
36         #     <TENSOR_NAME>.npy
37         #
38         # fq_dir/
39         #   tensors.txt
40         #   <DATA_INDEX>/
41         #     <TENSOR_NAME>.npy
42         self._num_data = len(list(filter(os.path.isdir, glob.glob(fp32_dir + '/*'))))
43         if self._num_data != len(list(filter(os.path.isdir, glob.glob(fq_dir + '/*')))):
44             raise RuntimeError("Number of data mistmatches")
45
46         self._num_processed_data += self._num_data
47
48         self._filename_to_tensor = dict()
49         with open(Path(fp32_dir) / 'tensors.txt') as f:
50             tensors = set([line.rstrip() for line in f])
51             for tensor in tensors:
52                 # Check if filename is unique
53                 # Fix name finding logic unless
54                 assert to_filename(tensor) not in self._filename_to_tensor
55                 self._filename_to_tensor[to_filename(tensor)] = tensor
56
57         # Save paths to fp32 data and fq data for each tensor
58         # dict
59         # {
60         #   <tensor_name>: (fp32_path, fq_path),
61         #   <tensor_name>: (fp32_path, fq_path),
62         #   ...
63         # }
64         data_paths = dict()
65         for data_idx in range(self._num_data):
66             fp32_results = glob.glob(fp32_dir + '/' + str(data_idx) + '/*.npy')
67             for fp32_data_path in fp32_results:
68                 fp32_path = Path(fp32_data_path)
69                 fq_data_path = fq_dir + '/' + str(data_idx) + '/' + fp32_path.with_suffix(
70                     '.npy').name
71                 fq_path = Path(fq_data_path)
72                 filename = fp32_path.stem
73                 tensor_name = self._filename_to_tensor[filename]
74
75                 # Only save the tensors which have both fp32 data and fq data
76                 if fq_path.is_file() and fp32_path.is_file():
77                     if tensor_name in data_paths:
78                         data_paths[tensor_name].append((fp32_data_path, fq_data_path))
79                     else:
80                         data_paths[tensor_name] = [(fp32_data_path, fq_data_path)]
81
82         return data_paths
83
84     def run(self):
85         '''Return qerror map (dict: tensor_name(string) -> qerror(float)).'''
86         raise NotImplementedError  # Child must implement this
87
88
89 class MPEIRComputer(QErrorComputer):
90     def __init__(self, fp32_dir, fq_dir):
91         super().__init__(fp32_dir, fq_dir)
92
93     # Incrementally compute Qerror while traversing all data in fp32_dir and fq_dir
94     def advance_on(self, fp32_dir, fq_dir):
95         data_paths = self.collect_data_path(fp32_dir, fq_dir)
96         for tensor_name, data_path in data_paths.items():
97             for (fp32_data_path, fq_data_path) in data_path:
98                 fp32_data = np.load(fp32_data_path)
99                 fq_data = np.load(fq_data_path)
100
101                 diff = np.absolute(fp32_data - fq_data).reshape(-1)
102
103                 fp32_min = np.min(fp32_data.reshape(-1))
104                 fp32_max = np.max(fp32_data.reshape(-1))
105
106                 # Peak Error-to-Interval Ratio (PEIR)
107                 # NOTE: PEIR is an analogue of PSNR (Peak Signal to Noise Ratio)
108                 PEAK_ERROR = np.max(diff)
109                 INTERVAL = fp32_max - fp32_min
110
111                 # If INTERVAL is 0, PEIR becomes NaN.
112                 # To prevent this, relaxed PEIR with epsilon(10^(-6)) is used.
113                 rPEIR = PEAK_ERROR / (INTERVAL + 0.000001)
114
115                 if tensor_name in self.qerror_map:
116                     self.qerror_map[tensor_name] += rPEIR
117                 else:
118                     self.qerror_map[tensor_name] = rPEIR
119
120     def get_final_result(self):
121         qerror_map = dict()
122         for tensor_name, acc in self.qerror_map.items():
123             qerror_map[tensor_name] = acc / self._num_processed_data
124
125         return qerror_map
126
127     def run(self):
128         self.advance_on(self._fp32_dir, self._fq_dir)
129         return self.get_final_result()
130
131
132 class MSEComputer(QErrorComputer):
133     def __init__(self, fp32_dir, fq_dir):
134         super().__init__(fp32_dir, fq_dir)
135         self.qerror_min = float('inf')
136         self.qerror_max = -self.qerror_min
137
138     # Incrementally compute Qerror while traversing all data in fp32_dir and fq_dir
139     def advance_on(self, fp32_dir, fq_dir):
140         data_paths = self.collect_data_path(fp32_dir, fq_dir)
141         for tensor_name, data_path in data_paths.items():
142             for (fp32_data_path, fq_data_path) in data_path:
143                 fp32_data = np.load(fp32_data_path)
144                 fq_data = np.load(fq_data_path)
145
146                 MSE = np.square(fp32_data - fq_data).mean()
147
148                 if tensor_name in self.qerror_map:
149                     self.qerror_map[tensor_name] += MSE
150                 else:
151                     self.qerror_map[tensor_name] = MSE
152
153                 self.qerror_min = min(MSE, self.qerror_min)
154                 self.qerror_max = max(MSE, self.qerror_max)
155
156     def get_final_result(self):
157         qerror_map = dict()
158         for tensor_name, acc in self.qerror_map.items():
159             qerror_map[tensor_name] = acc / self._num_processed_data
160
161         return qerror_map, self.qerror_min, self.qerror_max
162
163     def run(self):
164         self.advance_on(self._fp32_dir, self._fq_dir)
165         return self.get_final_result()
166
167
168 class TAEComputer(QErrorComputer):  #total absolute error
169     def __init__(self, fp32_dir, fq_dir):
170         super().__init__(fp32_dir, fq_dir)
171         self.total_error = 0
172         self.qerror_min = float('inf')
173         self.qerror_max = -self.qerror_min
174
175     def advance_on(self, fp32_dir, fq_dir):
176         data_paths = self.collect_data_path(fp32_dir, fq_dir)
177         for tensor_name, data_path in data_paths.items():
178             for (fp32_data_path, fq_data_path) in data_path:
179                 fp32_data = np.load(fp32_data_path)
180                 fq_data = np.load(fq_data_path)
181
182                 total_error = np.sum(np.abs(fp32_data - fq_data))
183
184                 if tensor_name in self.qerror_map:
185                     self.qerror_map[tensor_name] += total_error
186                 else:
187                     self.qerror_map[tensor_name] = total_error
188
189                 self.qerror_min = min(total_error, self.qerror_min)
190                 self.qerror_max = max(total_error, self.qerror_max)
191
192     def get_final_result(self):
193         qerror_map = dict()
194         for tensor_name, acc in self.qerror_map.items():
195             qerror_map[tensor_name] = acc / self._num_processed_data
196         return qerror_map, self.qerror_min, self.qerror_max
197
198     def run(self):
199         self.advance_on(self._fp32_dir, self._fq_dir)
200         return self.get_final_result()