Imported Upstream version 1.25.0
[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 import json
19
20 from pathlib import Path
21 from collections import defaultdict
22
23
24 class QErrorComputer:
25     def __init__(self, fp32_dir, fq_dir):
26         self._fp32_dir = fp32_dir
27         self._fq_dir = fq_dir
28         self.qerror_map = defaultdict(float)
29         self._num_processed_data = 0
30
31     def collect_data_path(self, fp32_dir, fq_dir):
32         # Assumption: FM data are saved as follows
33         #
34         # fp32_dir/
35         #   tensors.json
36         #   <DATA_INDEX>/
37         #     <TENSOR_ID>.npy
38         #
39         # fq_dir/
40         #   tensors.json
41         #   <DATA_INDEX>/
42         #     <TENSOR_ID>.npy
43         #
44         # NOTE tensors.json has a dictionary {TENSOR_NAME -> TENSOR_ID}
45         self._num_data = len(list(filter(os.path.isdir, glob.glob(fp32_dir + '/*'))))
46         if self._num_data != len(list(filter(os.path.isdir, glob.glob(fq_dir + '/*')))):
47             raise RuntimeError("Number of data mistmatches")
48
49         self._num_processed_data += self._num_data
50
51         self._tid_to_tname = dict()  # {tensor id -> tensor name}
52         with open(Path(fp32_dir) / 'tensors.json') as f:
53             tname_to_tid = json.load(f)
54
55         for tname, tid in tname_to_tid.items():
56             self._tid_to_tname[tid] = tname
57
58         # Save paths to fp32 data and fq data for each tensor
59         # dict
60         # {
61         #   <tensor_name>: (fp32_path, fq_path),
62         #   <tensor_name>: (fp32_path, fq_path),
63         #   ...
64         # }
65         data_paths = dict()
66         for data_idx in range(self._num_data):
67             fp32_results = glob.glob(fp32_dir + '/' + str(data_idx) + '/*.npy')
68             for fp32_data_path in fp32_results:
69                 fp32_path = Path(fp32_data_path)
70                 fq_data_path = fq_dir + '/' + str(data_idx) + '/' + fp32_path.with_suffix(
71                     '.npy').name
72                 fq_path = Path(fq_data_path)
73                 tid = int(fp32_path.stem)
74                 tensor_name = self._tid_to_tname[tid]
75
76                 # Only save the tensors which have both fp32 data and fq data
77                 if fq_path.is_file() and fp32_path.is_file():
78                     if tensor_name in data_paths:
79                         data_paths[tensor_name].append((fp32_data_path, fq_data_path))
80                     else:
81                         data_paths[tensor_name] = [(fp32_data_path, fq_data_path)]
82
83         return data_paths
84
85     def run(self):
86         '''Return qerror map (dict: tensor_name(string) -> qerror(float)).'''
87         raise NotImplementedError  # Child must implement this
88
89
90 class MPEIRComputer(QErrorComputer):
91     def __init__(self, fp32_dir, fq_dir):
92         super().__init__(fp32_dir, fq_dir)
93
94     # Incrementally compute Qerror while traversing all data in fp32_dir and fq_dir
95     def advance_on(self, fp32_dir, fq_dir):
96         data_paths = self.collect_data_path(fp32_dir, fq_dir)
97         for tensor_name, data_path in data_paths.items():
98             for (fp32_data_path, fq_data_path) in data_path:
99                 fp32_data = np.load(fp32_data_path)
100                 fq_data = np.load(fq_data_path)
101
102                 diff = np.absolute(fp32_data - fq_data).reshape(-1)
103
104                 fp32_min = np.min(fp32_data.reshape(-1))
105                 fp32_max = np.max(fp32_data.reshape(-1))
106
107                 # Peak Error-to-Interval Ratio (PEIR)
108                 # NOTE: PEIR is an analogue of PSNR (Peak Signal to Noise Ratio)
109                 PEAK_ERROR = np.max(diff)
110                 INTERVAL = fp32_max - fp32_min
111
112                 # If INTERVAL is 0, PEIR becomes NaN.
113                 # To prevent this, relaxed PEIR with epsilon(10^(-6)) is used.
114                 rPEIR = PEAK_ERROR / (INTERVAL + 0.000001)
115
116                 self.qerror_map[tensor_name] += rPEIR
117
118     # Return
119     # qerror_map (dict: tensor_name(string) -> qerror(float))
120     # qerror_min (float)
121     # qerror_max (float)
122     def get_final_result(self):
123         qerror_map = dict()
124         for tensor_name, acc in self.qerror_map.items():
125             qerror_map[tensor_name] = acc / self._num_processed_data
126
127         # Fixed qerror_min (0), qerror_max (1)
128         return qerror_map, 0.0, 1.0
129
130     def run(self):
131         self.advance_on(self._fp32_dir, self._fq_dir)
132         return self.get_final_result()
133
134
135 class MSEComputer(QErrorComputer):
136     def __init__(self, fp32_dir, fq_dir):
137         super().__init__(fp32_dir, fq_dir)
138         self.qerror_min = float('inf')
139         self.qerror_max = -self.qerror_min
140
141     # Incrementally compute Qerror while traversing all data in fp32_dir and fq_dir
142     def advance_on(self, fp32_dir, fq_dir):
143         data_paths = self.collect_data_path(fp32_dir, fq_dir)
144         for tensor_name, data_path in data_paths.items():
145             for (fp32_data_path, fq_data_path) in data_path:
146                 fp32_data = np.load(fp32_data_path)
147                 fq_data = np.load(fq_data_path)
148
149                 MSE = np.square(fp32_data - fq_data).mean()
150
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     # Return
157     # qerror_map (dict: tensor_name(string) -> qerror(float))
158     # qerror_min (float)
159     # qerror_max (float)
160     def get_final_result(self):
161         qerror_map = dict()
162         for tensor_name, acc in self.qerror_map.items():
163             qerror_map[tensor_name] = acc / self._num_processed_data
164
165         return qerror_map, self.qerror_min, self.qerror_max
166
167     def run(self):
168         self.advance_on(self._fp32_dir, self._fq_dir)
169         return self.get_final_result()
170
171
172 class TAEComputer(QErrorComputer):  #total absolute error
173     def __init__(self, fp32_dir, fq_dir):
174         super().__init__(fp32_dir, fq_dir)
175         self.total_error = 0
176         self.qerror_min = float('inf')
177         self.qerror_max = -self.qerror_min
178
179     def advance_on(self, fp32_dir, fq_dir):
180         data_paths = self.collect_data_path(fp32_dir, fq_dir)
181         for tensor_name, data_path in data_paths.items():
182             for (fp32_data_path, fq_data_path) in data_path:
183                 fp32_data = np.load(fp32_data_path)
184                 fq_data = np.load(fq_data_path)
185
186                 total_error = np.sum(np.abs(fp32_data - fq_data))
187
188                 self.qerror_map[tensor_name] += total_error
189
190                 self.qerror_min = min(total_error, self.qerror_min)
191                 self.qerror_max = max(total_error, self.qerror_max)
192
193     # Return
194     # qerror_map (dict: tensor_name(string) -> qerror(float))
195     # qerror_min (float)
196     # qerror_max (float)
197     def get_final_result(self):
198         qerror_map = dict()
199         for tensor_name, acc in self.qerror_map.items():
200             qerror_map[tensor_name] = acc / self._num_processed_data
201         return qerror_map, self.qerror_min, self.qerror_max
202
203     def run(self):
204         self.advance_on(self._fp32_dir, self._fq_dir)
205         return self.get_final_result()
206
207
208 # Scaled Root Mean Square Error (SRMSE)
209 # SRMSE = sqrt(MSE) / scale
210 class SRMSEComputer(QErrorComputer):
211     def __init__(self, fp32_dir, fq_dir):
212         super().__init__(fp32_dir, fq_dir)
213         if fq_dir != None:
214             self.scale_file = Path(fq_dir) / 'scales.txt'
215
216     # Incrementally compute Qerror while traversing all data in fp32_dir and fq_dir
217     def advance_on(self, fp32_dir, fq_dir):
218         if fq_dir != None:
219             self.scale_file = Path(fq_dir) / 'scales.txt'
220             self._fq_dir = fq_dir
221
222         data_paths = self.collect_data_path(fp32_dir, fq_dir)
223
224         for tensor_name, data_path in data_paths.items():
225             for (fp32_data_path, fq_data_path) in data_path:
226                 fp32_data = np.load(fp32_data_path)
227                 fq_data = np.load(fq_data_path)
228
229                 MSE = np.square(fp32_data - fq_data).mean()
230
231                 self.qerror_map[tensor_name] += MSE
232
233     # Return
234     # qerror_map (dict: tensor_name(string) -> qerror(float))
235     # qerror_min (float)
236     # qerror_max (float)
237     def get_final_result(self):
238         with open(self.scale_file) as f:
239             # scale_map: {tensor_name(str) -> scale(float)}
240             scale_map = json.load(f)
241
242         qerror_max = 0.0
243         qerror_map = dict()
244         for tensor_name, acc in self.qerror_map.items():
245             MSE = acc / self._num_processed_data
246             SRMSE = np.sqrt(MSE) / scale_map[tensor_name]
247             qerror_map[tensor_name] = SRMSE
248             qerror_max = max(SRMSE, qerror_max)
249
250         return qerror_map, 0.0, qerror_max
251
252     def run(self):
253         self.advance_on(self._fp32_dir, self._fq_dir)
254         return self.get_final_result()