test_settings.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  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. #
  17. import os
  18. import sys
  19. import json
  20. import math
  21. import subprocess
  22. import keras
  23. from abc import ABC, abstractmethod
  24. from packaging import version
  25. import numpy as np
  26. import tensorflow as tf
  27. import tf_keras as keras
  28. class TestSettings(ABC):
  29. # This is the generated test data used by the test cases.
  30. OUTDIR = 'TestCases/TestData/'
  31. # This is input to the data generation. If everything or something is regenerated then it is overwritten.
  32. # So it always has the same data as the OUTDIR.
  33. # The purpose of the pregen is primarily for debugging, as it is enabling to change a single parameter and see how
  34. # output changes (or not changes), without regenerating all input data.
  35. # It also convinient when testing changes in the script, to be able to run all test sets again.
  36. PREGEN = 'PregeneratedData/'
  37. INT32_MAX = 2147483647
  38. INT32_MIN = -2147483648
  39. INT64_MAX = 9223372036854775807
  40. INT64_MIN = -9223372036854775808
  41. INT16_MAX = 32767
  42. INT16_MIN = -32768
  43. INT8_MAX = 127
  44. INT8_MIN = -128
  45. INT4_MAX = 7
  46. INT4_MIN = -8
  47. REQUIRED_MINIMUM_TENSORFLOW_VERSION = version.parse("2.10")
  48. CLANG_FORMAT = 'clang-format-12 -i' # For formatting generated headers.
  49. def __init__(self,
  50. dataset,
  51. testtype,
  52. regenerate_weights,
  53. regenerate_input,
  54. regenerate_biases,
  55. schema_file,
  56. in_ch,
  57. out_ch,
  58. x_in,
  59. y_in,
  60. w_x,
  61. w_y,
  62. stride_x=1,
  63. stride_y=1,
  64. pad=False,
  65. randmin=np.iinfo(np.dtype('int8')).min,
  66. randmax=np.iinfo(np.dtype('int8')).max,
  67. batches=1,
  68. generate_bias=True,
  69. relu6=False,
  70. out_activation_min=None,
  71. out_activation_max=None,
  72. int16xint8=False,
  73. bias_min=np.iinfo(np.dtype('int32')).min,
  74. bias_max=np.iinfo(np.dtype('int32')).max,
  75. dilation_x=1,
  76. dilation_y=1,
  77. interpreter="tensorflow",
  78. int4_weights=False):
  79. self.int4_weights = int4_weights
  80. if self.INT8_MIN != np.iinfo(np.dtype('int8')).min or self.INT8_MAX != np.iinfo(np.dtype('int8')).max or \
  81. self.INT16_MIN != np.iinfo(np.dtype('int16')).min or self.INT16_MAX != np.iinfo(np.dtype('int16')).max or \
  82. self.INT32_MIN != np.iinfo(np.dtype('int32')).min or self.INT32_MAX != np.iinfo(np.dtype('int32')).max:
  83. raise RuntimeError("Unexpected int min/max error")
  84. self.use_tflite_micro_interpreter = False
  85. if interpreter == "tflite_runtime":
  86. from tflite_runtime.interpreter import Interpreter
  87. from tflite_runtime.interpreter import OpResolverType
  88. import tflite_runtime as tfl_runtime
  89. revision = tfl_runtime.__git_version__
  90. version = tfl_runtime.__version__
  91. interpreter = "tflite_runtime"
  92. elif interpreter == "tensorflow":
  93. from tensorflow.lite.python.interpreter import Interpreter
  94. from tensorflow.lite.python.interpreter import OpResolverType
  95. revision = tf.__git_version__
  96. version = tf.__version__
  97. interpreter = "tensorflow"
  98. elif interpreter == "tflite_micro":
  99. from tensorflow.lite.python.interpreter import Interpreter
  100. from tensorflow.lite.python.interpreter import OpResolverType
  101. import tflite_micro
  102. self.tflite_micro = tflite_micro
  103. self.use_tflite_micro_interpreter = True
  104. revision = None
  105. version = tflite_micro.__version__
  106. interpreter = "tflite_micro"
  107. else:
  108. raise RuntimeError(f"Invalid interpreter {interpreter}")
  109. self.Interpreter = Interpreter
  110. self.OpResolverType = OpResolverType
  111. self.tensorflow_reference_version = (
  112. "// Generated by {} using tensorflow version {} (Keras version {}).\n".format(
  113. os.path.basename(__file__), tf.__version__, keras.__version__))
  114. self.tensorflow_reference_version += ("// Interpreter from {} version {} and revision {}.\n".format(
  115. interpreter, version, revision))
  116. # Randomization interval
  117. self.mins = randmin
  118. self.maxs = randmax
  119. self.bias_mins = bias_min
  120. self.bias_maxs = bias_max
  121. self.input_ch = in_ch
  122. self.output_ch = out_ch
  123. self.x_input = x_in
  124. self.y_input = y_in
  125. self.filter_x = w_x
  126. self.filter_y = w_y
  127. self.stride_x = stride_x
  128. self.stride_y = stride_y
  129. self.dilation_x = dilation_x
  130. self.dilation_y = dilation_y
  131. self.batches = batches
  132. self.test_type = testtype
  133. self.has_padding = pad
  134. self.is_int16xint8 = int16xint8
  135. if relu6:
  136. self.out_activation_max = 6
  137. self.out_activation_min = 0
  138. else:
  139. if out_activation_min is not None:
  140. self.out_activation_min = out_activation_min
  141. else:
  142. self.out_activation_min = self.INT16_MIN if self.is_int16xint8 else self.INT8_MIN
  143. if out_activation_max is not None:
  144. self.out_activation_max = out_activation_max
  145. else:
  146. self.out_activation_max = self.INT16_MAX if self.is_int16xint8 else self.INT8_MAX
  147. # Bias is optional.
  148. self.generate_bias = generate_bias
  149. self.generated_header_files = []
  150. self.pregenerated_data_dir = self.PREGEN
  151. self.config_data = "config_data.h"
  152. self.testdataset = dataset
  153. self.kernel_table_file = self.pregenerated_data_dir + self.testdataset + '/' + 'kernel.txt'
  154. self.inputs_table_file = self.pregenerated_data_dir + self.testdataset + '/' + 'input.txt'
  155. self.bias_table_file = self.pregenerated_data_dir + self.testdataset + '/' + 'bias.txt'
  156. if self.has_padding:
  157. self.padding = 'SAME'
  158. else:
  159. self.padding = 'VALID'
  160. self.regenerate_new_weights = regenerate_weights
  161. self.regenerate_new_input = regenerate_input
  162. self.regenerate_new_bias = regenerate_biases
  163. self.schema_file = schema_file
  164. self.headers_dir = self.OUTDIR + self.testdataset + '/'
  165. os.makedirs(self.headers_dir, exist_ok=True)
  166. self.model_path = "{}model_{}".format(self.headers_dir, self.testdataset)
  167. self.model_path_tflite = self.model_path + '.tflite'
  168. self.input_data_file_prefix = "input"
  169. self.weight_data_file_prefix = "weights"
  170. self.bias_data_file_prefix = "biases"
  171. self.output_data_file_prefix = "output_ref"
  172. def save_multiple_dim_array_in_txt(self, file, data):
  173. header = ','.join(map(str, data.shape))
  174. np.savetxt(file, data.reshape(-1, data.shape[-1]), header=header, delimiter=',')
  175. def load_multiple_dim_array_from_txt(self, file):
  176. with open(file) as f:
  177. shape = list(map(int, next(f)[1:].split(',')))
  178. data = np.genfromtxt(f, delimiter=',').reshape(shape)
  179. return data.astype(np.float32)
  180. def convert_tensor_np(self, tensor_in, converter, *qminmax):
  181. w = tensor_in.numpy()
  182. shape = w.shape
  183. w = w.ravel()
  184. if len(qminmax) == 2:
  185. fw = converter(w, qminmax[0], qminmax[1])
  186. else:
  187. fw = converter(w)
  188. fw.shape = shape
  189. return tf.convert_to_tensor(fw)
  190. def convert_tensor(self, tensor_in, converter, *qminmax):
  191. w = tensor_in.numpy()
  192. shape = w.shape
  193. w = w.ravel()
  194. normal = np.array(w)
  195. float_normal = []
  196. for i in normal:
  197. if len(qminmax) == 2:
  198. float_normal.append(converter(i, qminmax[0], qminmax[1]))
  199. else:
  200. float_normal.append(converter(i))
  201. np_float_array = np.asarray(float_normal)
  202. np_float_array.shape = shape
  203. return tf.convert_to_tensor(np_float_array)
  204. def get_randomized_data(self, dims, npfile, regenerate, decimals=0, minrange=None, maxrange=None):
  205. if not minrange:
  206. minrange = self.mins
  207. if not maxrange:
  208. maxrange = self.maxs
  209. if not os.path.exists(npfile) or regenerate:
  210. regendir = os.path.dirname(npfile)
  211. os.makedirs(regendir, exist_ok=True)
  212. if decimals == 0:
  213. data = tf.Variable(tf.random.uniform(dims, minval=minrange, maxval=maxrange, dtype=tf.dtypes.int64))
  214. data = tf.cast(data, dtype=tf.float32)
  215. else:
  216. data = tf.Variable(tf.random.uniform(dims, minval=minrange, maxval=maxrange, dtype=tf.dtypes.float32))
  217. data = np.around(data.numpy(), decimals)
  218. data = tf.convert_to_tensor(data)
  219. print("Saving data to {}".format(npfile))
  220. self.save_multiple_dim_array_in_txt(npfile, data.numpy())
  221. else:
  222. print("Loading data from {}".format(npfile))
  223. data = tf.convert_to_tensor(self.load_multiple_dim_array_from_txt(npfile))
  224. return data
  225. def get_randomized_input_data(self, input_data, input_shape=None):
  226. # Generate or load saved input data unless hardcoded data provided
  227. if input_shape is None:
  228. input_shape = [self.batches, self.y_input, self.x_input, self.input_ch]
  229. if input_data is not None:
  230. input_data = tf.reshape(input_data, input_shape)
  231. else:
  232. input_data = self.get_randomized_data(input_shape,
  233. self.inputs_table_file,
  234. regenerate=self.regenerate_new_input)
  235. return input_data
  236. def get_randomized_bias_data(self, biases):
  237. # Generate or load saved bias data unless hardcoded data provided
  238. if not self.generate_bias:
  239. biases = tf.reshape(np.full([self.output_ch], 0), [self.output_ch])
  240. elif biases is not None:
  241. biases = tf.reshape(biases, [self.output_ch])
  242. else:
  243. biases = self.get_randomized_data([self.output_ch],
  244. self.bias_table_file,
  245. regenerate=self.regenerate_new_bias,
  246. minrange=self.bias_mins,
  247. maxrange=self.bias_maxs)
  248. return biases
  249. def format_output_file(self, file):
  250. command_list = self.CLANG_FORMAT.split(' ')
  251. command_list.append(file)
  252. try:
  253. process = subprocess.run(command_list)
  254. if process.returncode != 0:
  255. print(f"ERROR: {command_list = }")
  256. sys.exit(1)
  257. except Exception as e:
  258. raise RuntimeError(f"{e} from: {command_list = }")
  259. def write_c_header_wrapper(self):
  260. filename = "test_data.h"
  261. filepath = self.headers_dir + filename
  262. print("Generating C header wrapper {}...".format(filepath))
  263. with open(filepath, 'w+') as f:
  264. f.write(self.tensorflow_reference_version)
  265. while len(self.generated_header_files) > 0:
  266. f.write('#include "{}"\n'.format(self.generated_header_files.pop()))
  267. self.format_output_file(filepath)
  268. def write_common_config(self, f, prefix):
  269. """
  270. Shared by conv/depthwise_conv and pooling
  271. """
  272. f.write("#define {}_FILTER_X {}\n".format(prefix, self.filter_x))
  273. f.write("#define {}_FILTER_Y {}\n".format(prefix, self.filter_y))
  274. f.write("#define {}_STRIDE_X {}\n".format(prefix, self.stride_x))
  275. f.write("#define {}_STRIDE_Y {}\n".format(prefix, self.stride_y))
  276. f.write("#define {}_PAD_X {}\n".format(prefix, self.pad_x))
  277. f.write("#define {}_PAD_Y {}\n".format(prefix, self.pad_y))
  278. f.write("#define {}_OUTPUT_W {}\n".format(prefix, self.x_output))
  279. f.write("#define {}_OUTPUT_H {}\n".format(prefix, self.y_output))
  280. def write_c_common_header(self, f):
  281. f.write(self.tensorflow_reference_version)
  282. f.write("#pragma once\n")
  283. def write_c_config_header(self, write_common_parameters=True) -> None:
  284. filename = self.config_data
  285. self.generated_header_files.append(filename)
  286. filepath = self.headers_dir + filename
  287. prefix = self.testdataset.upper()
  288. print("Writing C header with config data {}...".format(filepath))
  289. with open(filepath, "w+") as f:
  290. self.write_c_common_header(f)
  291. if (write_common_parameters):
  292. f.write("#define {}_OUT_CH {}\n".format(prefix, self.output_ch))
  293. f.write("#define {}_IN_CH {}\n".format(prefix, self.input_ch))
  294. f.write("#define {}_INPUT_W {}\n".format(prefix, self.x_input))
  295. f.write("#define {}_INPUT_H {}\n".format(prefix, self.y_input))
  296. f.write("#define {}_DST_SIZE {}\n".format(
  297. prefix, self.x_output * self.y_output * self.output_ch * self.batches))
  298. f.write("#define {}_INPUT_SIZE {}\n".format(prefix, self.x_input * self.y_input * self.input_ch))
  299. f.write("#define {}_OUT_ACTIVATION_MIN {}\n".format(prefix, self.out_activation_min))
  300. f.write("#define {}_OUT_ACTIVATION_MAX {}\n".format(prefix, self.out_activation_max))
  301. f.write("#define {}_INPUT_BATCHES {}\n".format(prefix, self.batches))
  302. self.format_output_file(filepath)
  303. def get_data_file_name_info(self, name_prefix) -> (str, str):
  304. filename = name_prefix + "_data.h"
  305. filepath = self.headers_dir + filename
  306. return filename, filepath
  307. def generate_c_array(self, name, array, datatype="int8_t", const="const ", pack=False) -> None:
  308. w = None
  309. if type(array) is list:
  310. w = array
  311. size = len(array)
  312. elif type(array) is np.ndarray:
  313. w = array
  314. w = w.ravel()
  315. size = w.size
  316. else:
  317. w = array.numpy()
  318. w = w.ravel()
  319. size = tf.size(array)
  320. if pack:
  321. size = (size // 2) + (size % 2)
  322. filename, filepath = self.get_data_file_name_info(name)
  323. self.generated_header_files.append(filename)
  324. print("Generating C header {}...".format(filepath))
  325. with open(filepath, "w+") as f:
  326. self.write_c_common_header(f)
  327. f.write("#include <stdint.h>\n\n")
  328. if size > 0:
  329. f.write(const + datatype + " " + self.testdataset + '_' + name + "[%d] =\n{\n" % size)
  330. for i in range(size - 1):
  331. f.write(" %d,\n" % w[i])
  332. f.write(" %d\n" % w[size - 1])
  333. f.write("};\n")
  334. else:
  335. f.write(const + datatype + " *" + self.testdataset + '_' + name + " = NULL;\n")
  336. self.format_output_file(filepath)
  337. def calculate_padding(self, x_output, y_output, x_input, y_input):
  338. if self.has_padding:
  339. # Take dilation into account.
  340. filter_x = (self.filter_x - 1) * self.dilation_x + 1
  341. filter_y = (self.filter_y - 1) * self.dilation_y + 1
  342. pad_along_width = max((x_output - 1) * self.stride_x + filter_x - x_input, 0)
  343. pad_along_height = max((y_output - 1) * self.stride_y + filter_y - y_input, 0)
  344. pad_top = pad_along_height // 2
  345. pad_left = pad_along_width // 2
  346. pad_top_offset = pad_along_height % 2
  347. pad_left_offset = pad_along_width % 2
  348. self.pad_y_with_offset = pad_top + pad_top_offset
  349. self.pad_x_with_offset = pad_left + pad_left_offset
  350. self.pad_x = pad_left
  351. self.pad_y = pad_top
  352. else:
  353. self.pad_x = 0
  354. self.pad_y = 0
  355. self.pad_y_with_offset = 0
  356. self.pad_x_with_offset = 0
  357. @abstractmethod
  358. def generate_data(self, input_data=None, weights=None, biases=None) -> None:
  359. ''' Must be overriden '''
  360. def quantize_scale(self, scale):
  361. significand, shift = math.frexp(scale)
  362. significand_q31 = round(significand * (1 << 31))
  363. return significand_q31, shift
  364. def get_calib_data_func(self, n_inputs, shape):
  365. def representative_data_gen():
  366. representative_testsets = []
  367. if n_inputs > 0:
  368. for i in range(n_inputs):
  369. representative_testsets.append(np.ones(shape, dtype=np.float32))
  370. yield representative_testsets
  371. else:
  372. raise RuntimeError("Invalid number of representative test sets: {}. Must be more than 0".format(
  373. self.test_type))
  374. return representative_data_gen
  375. def convert_and_interpret(self, model, inttype, input_data=None, dataset_shape=None):
  376. """
  377. Compile and convert a model to Tflite format, run interpreter and allocate tensors.
  378. """
  379. self.convert_model(model, inttype, dataset_shape)
  380. return self.interpret_model(input_data, inttype)
  381. def convert_model(self, model, inttype, dataset_shape=None):
  382. model.compile(loss=keras.losses.categorical_crossentropy,
  383. optimizer=keras.optimizers.Adam(),
  384. metrics=['accuracy'])
  385. n_inputs = len(model.inputs)
  386. if dataset_shape:
  387. representative_dataset_shape = dataset_shape
  388. else:
  389. representative_dataset_shape = (self.batches, self.y_input, self.x_input, self.input_ch)
  390. converter = tf.lite.TFLiteConverter.from_keras_model(model)
  391. representative_dataset = self.get_calib_data_func(n_inputs, representative_dataset_shape)
  392. converter.optimizations = [tf.lite.Optimize.DEFAULT]
  393. converter.representative_dataset = representative_dataset
  394. if self.is_int16xint8:
  395. converter.target_spec.supported_ops = [
  396. tf.lite.OpsSet.EXPERIMENTAL_TFLITE_BUILTINS_ACTIVATIONS_INT16_WEIGHTS_INT8
  397. ]
  398. else:
  399. converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
  400. converter.inference_input_type = inttype
  401. converter.inference_output_type = inttype
  402. tflite_model = converter.convert()
  403. os.makedirs(os.path.dirname(self.model_path_tflite), exist_ok=True)
  404. with open(self.model_path_tflite, "wb") as model:
  405. model.write(tflite_model)
  406. def interpret_model(self, input_data, inttype):
  407. interpreter = self.Interpreter(model_path=str(self.model_path_tflite),
  408. experimental_op_resolver_type=self.OpResolverType.BUILTIN_REF)
  409. interpreter.allocate_tensors()
  410. output_details = interpreter.get_output_details()
  411. (self.output_scale, self.output_zero_point) = output_details[0]['quantization']
  412. if input_data is not None:
  413. input_details = interpreter.get_input_details()
  414. (self.input_scale, self.input_zero_point) = input_details[0]['quantization']
  415. # Set input tensors
  416. interpreter.set_tensor(input_details[0]["index"], tf.cast(input_data, inttype))
  417. return interpreter
  418. # TODO: make it a more generic function and remove reference to svdf specific names
  419. def generate_json_from_template(self,
  420. weights_feature_data=None,
  421. weights_time_data=None,
  422. bias_data=None,
  423. int8_time_weights=False,
  424. bias_buffer=3):
  425. """
  426. Takes a json template and parameters as input and creates a new json file.
  427. """
  428. generated_json_file = self.model_path + '.json'
  429. with open(self.json_template, 'r') as in_file, open(generated_json_file, 'w') as out_file:
  430. # Update shapes, scales and zero points
  431. data = in_file.read()
  432. for item, to_replace in self.json_replacements.items():
  433. data = data.replace(item, str(to_replace))
  434. data = json.loads(data)
  435. # Update weights and bias data
  436. if weights_feature_data is not None:
  437. w_1_buffer_index = 1
  438. data["buffers"][w_1_buffer_index]["data"] = self.to_bytes(weights_feature_data.numpy().ravel(), 1)
  439. if weights_time_data is not None:
  440. w_2_buffer_index = 2
  441. if int8_time_weights:
  442. data["buffers"][w_2_buffer_index]["data"] = self.to_bytes(weights_time_data.numpy().ravel(), 1)
  443. else:
  444. data["buffers"][w_2_buffer_index]["data"] = self.to_bytes(weights_time_data.numpy().ravel(), 2)
  445. if bias_data is not None:
  446. bias_buffer_index = bias_buffer
  447. data["buffers"][bias_buffer_index]["data"] = self.to_bytes(bias_data.numpy().ravel(), 4)
  448. json.dump(data, out_file, indent=2)
  449. return generated_json_file
  450. def flatc_generate_tflite(self, json_input, schema):
  451. flatc = 'flatc'
  452. if schema is None:
  453. raise RuntimeError("A schema file is required.")
  454. command = "{} -o {} -c -b {} {}".format(flatc, self.headers_dir, schema, json_input)
  455. command_list = command.split(' ')
  456. try:
  457. process = subprocess.run(command_list)
  458. if process.returncode != 0:
  459. print(f"ERROR: {command = }")
  460. sys.exit(1)
  461. except Exception as e:
  462. raise RuntimeError(f"{e} from: {command = }. Did you install flatc?")
  463. def to_bytes(self, tensor_data, type_size) -> bytes:
  464. result_bytes = []
  465. if type_size == 1:
  466. tensor_type = np.uint8
  467. elif type_size == 2:
  468. tensor_type = np.uint16
  469. elif type_size == 4:
  470. tensor_type = np.uint32
  471. else:
  472. raise RuntimeError("Size not supported: {}".format(type_size))
  473. for val in tensor_data:
  474. for byte in int(tensor_type(val)).to_bytes(type_size, 'little'):
  475. result_bytes.append(byte)
  476. return result_bytes