1 # Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
20 from pathlib import Path
21 from collections import defaultdict
25 def __init__(self, fp32_dir, fq_dir):
26 self._fp32_dir = fp32_dir
28 self.qerror_map = defaultdict(float)
29 self._num_processed_data = 0
31 def collect_data_path(self, fp32_dir, fq_dir):
32 # Assumption: FM data are saved as follows
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")
49 self._num_processed_data += self._num_data
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)
55 for tname, tid in tname_to_tid.items():
56 self._tid_to_tname[tid] = tname
58 # Save paths to fp32 data and fq data for each tensor
61 # <tensor_name>: (fp32_path, fq_path),
62 # <tensor_name>: (fp32_path, fq_path),
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(
72 fq_path = Path(fq_data_path)
73 tid = int(fp32_path.stem)
74 tensor_name = self._tid_to_tname[tid]
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))
81 data_paths[tensor_name] = [(fp32_data_path, fq_data_path)]
86 '''Return qerror map (dict: tensor_name(string) -> qerror(float)).'''
87 raise NotImplementedError # Child must implement this
90 class MPEIRComputer(QErrorComputer):
91 def __init__(self, fp32_dir, fq_dir):
92 super().__init__(fp32_dir, fq_dir)
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)
102 diff = np.absolute(fp32_data - fq_data).reshape(-1)
104 fp32_min = np.min(fp32_data.reshape(-1))
105 fp32_max = np.max(fp32_data.reshape(-1))
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
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)
116 self.qerror_map[tensor_name] += rPEIR
119 # qerror_map (dict: tensor_name(string) -> qerror(float))
122 def get_final_result(self):
124 for tensor_name, acc in self.qerror_map.items():
125 qerror_map[tensor_name] = acc / self._num_processed_data
127 # Fixed qerror_min (0), qerror_max (1)
128 return qerror_map, 0.0, 1.0
131 self.advance_on(self._fp32_dir, self._fq_dir)
132 return self.get_final_result()
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
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)
149 MSE = np.square(fp32_data - fq_data).mean()
151 self.qerror_map[tensor_name] += MSE
153 self.qerror_min = min(MSE, self.qerror_min)
154 self.qerror_max = max(MSE, self.qerror_max)
157 # qerror_map (dict: tensor_name(string) -> qerror(float))
160 def get_final_result(self):
162 for tensor_name, acc in self.qerror_map.items():
163 qerror_map[tensor_name] = acc / self._num_processed_data
165 return qerror_map, self.qerror_min, self.qerror_max
168 self.advance_on(self._fp32_dir, self._fq_dir)
169 return self.get_final_result()
172 class TAEComputer(QErrorComputer): #total absolute error
173 def __init__(self, fp32_dir, fq_dir):
174 super().__init__(fp32_dir, fq_dir)
176 self.qerror_min = float('inf')
177 self.qerror_max = -self.qerror_min
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)
186 total_error = np.sum(np.abs(fp32_data - fq_data))
188 self.qerror_map[tensor_name] += total_error
190 self.qerror_min = min(total_error, self.qerror_min)
191 self.qerror_max = max(total_error, self.qerror_max)
194 # qerror_map (dict: tensor_name(string) -> qerror(float))
197 def get_final_result(self):
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
204 self.advance_on(self._fp32_dir, self._fq_dir)
205 return self.get_final_result()
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)
214 self.scale_file = Path(fq_dir) / 'scales.txt'
216 # Incrementally compute Qerror while traversing all data in fp32_dir and fq_dir
217 def advance_on(self, fp32_dir, fq_dir):
219 self.scale_file = Path(fq_dir) / 'scales.txt'
220 self._fq_dir = fq_dir
222 data_paths = self.collect_data_path(fp32_dir, fq_dir)
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)
229 MSE = np.square(fp32_data - fq_data).mean()
231 self.qerror_map[tensor_name] += MSE
234 # qerror_map (dict: tensor_name(string) -> qerror(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)
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)
250 return qerror_map, 0.0, qerror_max
253 self.advance_on(self._fp32_dir, self._fq_dir)
254 return self.get_final_result()