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