1# SPDX-FileCopyrightText: Copyright 2010-2024 Arm Limited and/or its affiliates <open-source-office@arm.com> 2# 3# SPDX-License-Identifier: Apache-2.0 4# 5# Licensed under the Apache License, Version 2.0 (the License); you may 6# not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# 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, WITHOUT 13# 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# 17from test_settings import TestSettings 18 19import tensorflow as tf 20import numpy as np 21import tf_keras as keras 22 23 24class FullyConnectedSettings(TestSettings): 25 26 def __init__(self, 27 dataset, 28 testtype, 29 regenerate_weights, 30 regenerate_input, 31 regenerate_biases, 32 schema_file, 33 in_ch=1, 34 out_ch=1, 35 x_in=1, 36 y_in=1, 37 w_x=1, 38 w_y=1, 39 stride_x=1, 40 stride_y=1, 41 pad=False, 42 randmin=TestSettings.INT8_MIN, 43 randmax=TestSettings.INT8_MAX, 44 batches=1, 45 generate_bias=True, 46 out_activation_min=None, 47 out_activation_max=None, 48 int16xint8=False, 49 bias_min=TestSettings.INT32_MIN, 50 bias_max=TestSettings.INT32_MAX, 51 interpreter="tensorflow", 52 input_scale=0.1, 53 input_zp=0, 54 w_scale=0.005, 55 w_zp=0, 56 bias_scale=0.00002, 57 bias_zp=0, 58 output_scale=0.1, 59 output_zp=0, 60 int4_weights=False 61 ): 62 super().__init__(dataset, 63 testtype, 64 regenerate_weights, 65 regenerate_input, 66 regenerate_biases, 67 schema_file, 68 in_ch, 69 out_ch, 70 x_in, 71 y_in, 72 x_in, 73 y_in, 74 stride_x, 75 stride_y, 76 pad, 77 randmin, 78 randmax, 79 batches, 80 generate_bias=generate_bias, 81 out_activation_min=out_activation_min, 82 out_activation_max=out_activation_max, 83 int16xint8=int16xint8, 84 bias_min=bias_min, 85 bias_max=bias_max, 86 interpreter=interpreter, 87 int4_weights=int4_weights) 88 89 self.filter_zero_point = w_zp 90 91 if self.int4_weights or self.filter_zero_point: 92 if self.generate_bias: 93 self.json_template = "TestCases/Common/fc_weights_template.json" 94 else: 95 self.json_template = "TestCases/Common/fc_weights_template_null_bias.json" 96 97 weight_type = "INT4" if self.int4_weights else "INT8" 98 99 self.json_replacements = { 100 "batches": batches, 101 "input_size": in_ch * x_in * y_in, 102 "input_scale": input_scale, 103 "input_zp": input_zp, 104 "w_type": weight_type, 105 "w_scale": w_scale, 106 "w_zp": w_zp, 107 "bias_size": out_ch, 108 "bias_scale": bias_scale, 109 "bias_zp": bias_zp, 110 "output_size": out_ch, 111 "output_scale": output_scale, 112 "output_zp": output_zp 113 } 114 115 def write_c_config_header(self) -> None: 116 super().write_c_config_header() 117 118 filename = self.config_data 119 filepath = self.headers_dir + filename 120 prefix = self.testdataset.upper() 121 122 with open(filepath, "a") as f: 123 f.write("#define {}_OUTPUT_MULTIPLIER {}\n".format(prefix, self.quantized_multiplier)) 124 f.write("#define {}_OUTPUT_SHIFT {}\n".format(prefix, self.quantized_shift)) 125 f.write("#define {}_ACCUMULATION_DEPTH {}\n".format(prefix, self.input_ch * self.x_input * self.y_input)) 126 f.write("#define {}_INPUT_OFFSET {}\n".format(prefix, -self.input_zero_point)) 127 f.write("#define {}_FILTER_OFFSET {}\n".format(prefix, -self.filter_zero_point)) 128 f.write("#define {}_OUTPUT_OFFSET {}\n".format(prefix, self.output_zero_point)) 129 130 def quantize_multiplier(self, weights_scale): 131 input_product_scale = self.input_scale * weights_scale 132 if input_product_scale < 0: 133 raise RuntimeError("negative input product scale") 134 real_multipler = input_product_scale / self.output_scale 135 (self.quantized_multiplier, self.quantized_shift) = self.quantize_scale(real_multipler) 136 137 def generate_data(self, input_data=None, weights=None, biases=None) -> None: 138 139 if self.is_int16xint8: 140 inttype = tf.int16 141 datatype = "int16_t" 142 bias_datatype = "int64_t" 143 else: 144 inttype = tf.int8 145 datatype = "int8_t" 146 bias_datatype = "int32_t" 147 148 # Generate data 149 fc_input_format = [self.batches, self.input_ch * self.x_input * self.y_input] 150 if input_data is not None: 151 input_data = tf.reshape(input_data, fc_input_format) 152 else: 153 input_data = self.get_randomized_input_data(input_data, fc_input_format) 154 155 # Generate bias 156 if self.generate_bias: 157 biases = self.get_randomized_bias_data(biases) 158 else: 159 biases = None 160 161 if self.filter_zero_point: 162 temp1 = self.model_path 163 temp2 = self.json_template 164 165 fc_weights_format = [self.input_ch * self.y_input * self.x_input * self.output_ch] 166 if weights is not None: 167 weights = tf.reshape(weights, fc_weights_format) 168 else: 169 weights = self.get_randomized_data(fc_weights_format, 170 self.kernel_table_file, 171 minrange=TestSettings.INT8_MIN, 172 maxrange=TestSettings.INT8_MAX, 173 regenerate=self.regenerate_new_weights) 174 175 self.model_path = self.model_path 176 self.json_template = self.json_template 177 generated_json = self.generate_json_from_template(weights, bias_data=biases, bias_buffer=2) 178 self.flatc_generate_tflite(generated_json, self.schema_file) 179 180 weights_size = weights.numpy().size 181 filter_index = 1 182 bias_index = 2 183 184 elif self.int4_weights: 185 # Generate weights, both packed and unpacked model from JSON 186 temp1 = self.model_path 187 temp2 = self.json_template 188 189 fc_weights_format = [self.input_ch * self.y_input * self.x_input * self.output_ch] 190 if weights is not None: 191 weights = tf.reshape(weights, fc_weights_format) 192 else: 193 weights = self.get_randomized_data(fc_weights_format, 194 self.kernel_table_file, 195 minrange=TestSettings.INT4_MIN, 196 maxrange=TestSettings.INT4_MAX, 197 regenerate=self.regenerate_new_weights) 198 199 # Unpacked model is used for reference during debugging only and not used by default 200 self.model_path = self.model_path + "_unpacked" 201 self.json_template = self.json_template[:-5] + "_unpacked.json" 202 generated_json = self.generate_json_from_template(weights, bias_data=biases, bias_buffer=2) 203 self.flatc_generate_tflite(generated_json, self.schema_file) 204 205 self.model_path = temp1 206 self.json_template = temp2 207 208 uneven = len(weights) % 2 209 if uneven: 210 weights = tf.experimental.numpy.append(weights, 0) 211 212 temp = np.reshape(weights, (len(weights) // 2, 2)).astype(np.uint8) 213 temp = 0xff & ((0xf0 & (temp[:, 1] << 4)) | (temp[:, 0] & 0xf)) 214 weights = tf.convert_to_tensor(temp) 215 weights_size = weights.numpy().size * 2 216 217 if uneven: 218 weights_size = weights_size - 1 219 220 generated_json = self.generate_json_from_template(weights, bias_data=biases, bias_buffer=2) 221 self.flatc_generate_tflite(generated_json, self.schema_file) 222 223 filter_index = 1 224 bias_index = 2 225 226 else: 227 fc_weights_format = [self.input_ch * self.y_input * self.x_input, self.output_ch] 228 if weights is not None: 229 weights = tf.reshape(weights, fc_weights_format) 230 else: 231 weights = self.get_randomized_data(fc_weights_format, 232 self.kernel_table_file, 233 minrange=TestSettings.INT32_MIN, 234 maxrange=TestSettings.INT32_MAX, 235 regenerate=self.regenerate_new_weights) 236 weights_size = weights.numpy().size 237 238 # Generate model in tensorflow with one fully_connected layer 239 model = keras.models.Sequential() 240 model.add( 241 keras.layers.InputLayer(input_shape=(self.y_input * self.x_input * self.input_ch, ), 242 batch_size=self.batches)) 243 fully_connected_layer = keras.layers.Dense(self.output_ch, activation=None, use_bias=self.generate_bias) 244 model.add(fully_connected_layer) 245 if self.generate_bias: 246 fully_connected_layer.set_weights([weights, biases]) 247 else: 248 fully_connected_layer.set_weights([weights]) 249 self.convert_model(model, inttype) 250 251 bias_index = 1 252 if self.generate_bias: 253 filter_index = 2 254 else: 255 filter_index = 1 256 257 interpreter = self.interpret_model(input_data, inttype) 258 259 # Get layer information 260 all_layers_details = interpreter.get_tensor_details() 261 filter_layer = all_layers_details[filter_index] 262 bias_layer = all_layers_details[bias_index] 263 264 if weights_size != interpreter.get_tensor(filter_layer['index']).size or \ 265 (self.generate_bias and biases.numpy().size != interpreter.get_tensor(bias_layer['index']).size): 266 raise RuntimeError(f"Dimension mismatch for {self.testdataset}") 267 268 weights_zero_point = filter_layer['quantization_parameters']['zero_points'][0] 269 if weights_zero_point != self.filter_zero_point: 270 raise RuntimeError(f"Filter zero point point mismatch for {self.filter_zero_point}") 271 272 self.x_output = 1 273 self.y_output = 1 274 275 weights_scale = filter_layer['quantization_parameters']['scales'][0] 276 self.quantize_multiplier(weights_scale) 277 278 # Generate reference output 279 output_details = interpreter.get_output_details() 280 interpreter.invoke() 281 output_data = interpreter.get_tensor(output_details[0]["index"]) 282 283 # Save results 284 self.generate_c_array(self.input_data_file_prefix, input_data, datatype=datatype) 285 self.generate_c_array( 286 self.weight_data_file_prefix, interpreter.get_tensor(filter_layer['index']), pack=self.int4_weights) 287 if not self.generate_bias: 288 bias = [] 289 else: 290 bias = interpreter.get_tensor(bias_layer['index']) 291 self.generate_c_array(self.bias_data_file_prefix, bias, datatype=bias_datatype) 292 293 self.generate_c_array(self.output_data_file_prefix, 294 np.clip(output_data, self.out_activation_min, self.out_activation_max), 295 datatype=datatype) 296 self.write_c_config_header() 297 self.write_c_header_wrapper() 298