1 /*
2 * Copyright (c) 2023-2024 Tomeu Vizoso <tomeu@tomeuvizoso.net>
3 * SPDX-License-Identifier: MIT
4 */
5
6 #include <cstdio>
7 #include <fcntl.h>
8 #include <filesystem>
9 #include <fstream>
10 #include <gtest/gtest.h>
11 #include <xtensor/xrandom.hpp>
12
13 #include <iostream>
14 #include "tensorflow/lite/c/c_api.h"
15 #include "test_executor.h"
16
17 #define TEST_CONV2D 1
18 #define TEST_DEPTHWISE 1
19 #define TEST_ADD 1
20 #define TEST_FULLY_CONNECTED 1
21 #define TEST_MOBILENETV1 1
22 #define TEST_MOBILEDET 1
23 #define TEST_YOLOX 1
24
25 #define TOLERANCE 2
26 #define MODEL_TOLERANCE 8
27 #define YOLOX_TOLERANCE 38
28 #define QUANT_TOLERANCE 2
29
30 std::vector<bool> is_signed{false}; /* TODO: Support INT8? */
31 std::vector<bool> padding_same{false, true};
32 std::vector<int> stride{1, 2};
33 std::vector<int> output_channels{1, 32, 120, 128, 160, 256};
34 std::vector<int> input_channels{1, 32, 120, 128, 256};
35 std::vector<int> dw_channels{1, 32, 120, 128, 256};
36 std::vector<int> dw_weight_size{3, 5};
37 std::vector<int> weight_size{1, 3, 5};
38 std::vector<int> input_size{3, 5, 8, 80, 112};
39 std::vector<int> fc_channels{23, 46, 128, 256, 512};
40 std::vector<int> fc_size{128, 1280, 25088, 62720};
41
42 static void
set_seed(unsigned seed)43 set_seed(unsigned seed)
44 {
45 srand(seed);
46 xt::random::seed(seed);
47 }
48
49 static void
test_model(void * buf,size_t buf_size,std::string cache_dir,unsigned tolerance)50 test_model(void *buf, size_t buf_size, std::string cache_dir, unsigned tolerance)
51 {
52 void **input = NULL;
53 size_t num_inputs;
54 void **cpu_output;
55 size_t *output_sizes;
56 TfLiteType *output_types;
57 size_t num_outputs;
58 void **npu_output;
59
60 TfLiteModel *model = TfLiteModelCreate(buf, buf_size);
61 assert(model);
62
63 run_model(model, EXECUTOR_CPU, &input, &num_inputs, &cpu_output, &output_sizes, &output_types, &num_outputs, cache_dir);
64 run_model(model, EXECUTOR_NPU, &input, &num_inputs, &npu_output, &output_sizes, &output_types, &num_outputs, cache_dir);
65
66 for (size_t i = 0; i < num_outputs; i++) {
67 for (size_t j = 0; j < output_sizes[i]; j++) {
68 switch (output_types[i]) {
69 case kTfLiteFloat32: {
70 float *cpu = ((float**)cpu_output)[i];
71 float *npu = ((float**)npu_output)[i];
72 if (abs(cpu[j] - npu[j]) > tolerance) {
73 std::cout << "CPU: ";
74 for (int k = 0; k < std::min(int(output_sizes[i]), 24); k++)
75 std::cout << std::setfill('0') << std::setw(6) << cpu[k] << " ";
76 std::cout << "\n";
77 std::cout << "NPU: ";
78 for (int k = 0; k < std::min(int(output_sizes[i]), 24); k++)
79 std::cout << std::setfill('0') << std::setw(6) << npu[k] << " ";
80 std::cout << "\n";
81
82 FAIL() << "Output at " << j << " from the NPU (" << std::setfill('0') << std::setw(2) << npu[j] << ") doesn't match that from the CPU (" << std::setfill('0') << std::setw(2) << cpu[j] << ").";
83 }
84 break;
85 }
86 default: {
87 uint8_t *cpu = ((uint8_t**)cpu_output)[i];
88 uint8_t *npu = ((uint8_t**)npu_output)[i];
89 if (abs(cpu[j] - npu[j]) > tolerance) {
90 std::cout << "CPU: ";
91 for (int k = 0; k < std::min(int(output_sizes[i]), 24); k++)
92 std::cout << std::setfill('0') << std::setw(2) << std::hex << int(cpu[k]) << " ";
93 std::cout << "\n";
94 std::cout << "NPU: ";
95 for (int k = 0; k < std::min(int(output_sizes[i]), 24); k++)
96 std::cout << std::setfill('0') << std::setw(2) << std::hex << int(npu[k]) << " ";
97 std::cout << "\n";
98
99 FAIL() << "Output at " << j << " from the NPU (" << std::setfill('0') << std::setw(2) << std::hex << int(npu[j]) << ") doesn't match that from the CPU (" << std::setfill('0') << std::setw(2) << std::hex << int(cpu[j]) << ").";
100 }
101 break;
102 }
103 }
104 }
105 }
106
107 for (size_t i = 0; i < num_inputs; i++)
108 free(input[i]);
109 free(input);
110
111 for (size_t i = 0; i < num_outputs; i++)
112 free(cpu_output[i]);
113 free(cpu_output);
114
115 for (size_t i = 0; i < num_outputs; i++)
116 free(npu_output[i]);
117 free(npu_output);
118
119 free(output_sizes);
120 free(output_types);
121
122 TfLiteModelDelete(model);
123 }
124
125 static void
test_model_file(std::string file_name,unsigned tolerance,bool use_cache)126 test_model_file(std::string file_name, unsigned tolerance, bool use_cache)
127 {
128 std::ostringstream cache_dir;
129
130 if (use_cache)
131 cache_dir << "/var/cache/teflon_tests/" << std::filesystem::path(file_name).stem().c_str();
132
133 set_seed(4);
134
135 std::ifstream model_file(file_name, std::ios::binary);
136 std::vector<uint8_t> buffer((std::istreambuf_iterator<char>(model_file)),
137 std::istreambuf_iterator<char>());
138 test_model(buffer.data(), buffer.size(), cache_dir.str(), tolerance);
139 }
140
141 void
test_conv(int input_size,int weight_size,int input_channels,int output_channels,int stride,bool padding_same,bool is_signed,bool depthwise,int seed)142 test_conv(int input_size, int weight_size, int input_channels, int output_channels,
143 int stride, bool padding_same, bool is_signed, bool depthwise, int seed)
144 {
145 void *buf = NULL;
146 size_t buf_size;
147 std::ostringstream cache_dir, model_cache;
148 cache_dir << "/var/cache/teflon_tests/" << input_size << "_" << weight_size << "_" << input_channels << "_" << output_channels << "_" << stride << "_" << padding_same << "_" << is_signed << "_" << depthwise << "_" << seed;
149 model_cache << cache_dir.str() << "/"
150 << "model.tflite";
151
152 if (weight_size > input_size)
153 GTEST_SKIP();
154
155 set_seed(seed);
156
157 if (cache_is_enabled()) {
158 if (access(model_cache.str().c_str(), F_OK) == 0) {
159 buf = read_buf(model_cache.str().c_str(), &buf_size);
160 }
161 }
162
163 if (buf == 0) {
164 buf = conv2d_generate_model(input_size, weight_size,
165 input_channels, output_channels,
166 stride, padding_same, is_signed,
167 depthwise,
168 &buf_size);
169
170 if (cache_is_enabled()) {
171 if (access(cache_dir.str().c_str(), F_OK) != 0) {
172 ASSERT_TRUE(std::filesystem::create_directories(cache_dir.str().c_str()));
173 }
174 std::ofstream file(model_cache.str().c_str(), std::ios::out | std::ios::binary);
175 file.write(reinterpret_cast<const char *>(buf), buf_size);
176 file.close();
177 }
178 }
179
180 test_model(buf, buf_size, cache_dir.str(), TOLERANCE);
181 free(buf);
182 }
183
184 void
test_add(int input_size,int weight_size,int input_channels,int output_channels,int stride,bool padding_same,bool is_signed,bool depthwise,int seed,unsigned tolerance)185 test_add(int input_size, int weight_size, int input_channels, int output_channels,
186 int stride, bool padding_same, bool is_signed, bool depthwise, int seed,
187 unsigned tolerance)
188 {
189 void *buf = NULL;
190 size_t buf_size;
191 std::ostringstream cache_dir, model_cache;
192 cache_dir << "/var/cache/teflon_tests/"
193 << "add_" << input_size << "_" << weight_size << "_" << input_channels << "_" << output_channels << "_" << stride << "_" << padding_same << "_" << is_signed << "_" << depthwise << "_" << seed;
194 model_cache << cache_dir.str() << "/"
195 << "model.tflite";
196
197 if (weight_size > input_size)
198 GTEST_SKIP();
199
200 set_seed(seed);
201
202 if (cache_is_enabled()) {
203 if (access(model_cache.str().c_str(), F_OK) == 0) {
204 buf = read_buf(model_cache.str().c_str(), &buf_size);
205 }
206 }
207
208 if (buf == 0) {
209 buf = add_generate_model(input_size, weight_size,
210 input_channels, output_channels,
211 stride, padding_same, is_signed,
212 depthwise,
213 &buf_size);
214
215 if (cache_is_enabled()) {
216 if (access(cache_dir.str().c_str(), F_OK) != 0) {
217 ASSERT_TRUE(std::filesystem::create_directories(cache_dir.str().c_str()));
218 }
219 std::ofstream file(model_cache.str().c_str(), std::ios::out | std::ios::binary);
220 file.write(reinterpret_cast<const char *>(buf), buf_size);
221 file.close();
222 }
223 }
224
225 test_model(buf, buf_size, cache_dir.str(), tolerance);
226 free(buf);
227 }
228
229 void
test_fully_connected(int input_size,int output_channels,bool is_signed,int seed)230 test_fully_connected(int input_size, int output_channels, bool is_signed, int seed)
231 {
232 void *buf = NULL;
233 size_t buf_size;
234 std::ostringstream cache_dir, model_cache;
235 cache_dir << "/var/cache/teflon_tests/fc_" << input_size << "_" << output_channels << "_" << is_signed << "_" << seed;
236 model_cache << cache_dir.str() << "/"
237 << "model.tflite";
238
239 set_seed(seed);
240
241 if (cache_is_enabled()) {
242 if (access(model_cache.str().c_str(), F_OK) == 0) {
243 buf = read_buf(model_cache.str().c_str(), &buf_size);
244 }
245 }
246
247 if (buf == 0) {
248 buf = fully_connected_generate_model(input_size, output_channels, is_signed, &buf_size);
249
250 if (cache_is_enabled()) {
251 if (access(cache_dir.str().c_str(), F_OK) != 0) {
252 ASSERT_TRUE(std::filesystem::create_directories(cache_dir.str().c_str()));
253 }
254 std::ofstream file(model_cache.str().c_str(), std::ios::out | std::ios::binary);
255 file.write(reinterpret_cast<const char *>(buf), buf_size);
256 file.close();
257 }
258 }
259
260 test_model(buf, buf_size, cache_dir.str(), TOLERANCE);
261 free(buf);
262 }
263
264 #if TEST_CONV2D
265
266 class Conv2D : public testing::TestWithParam<std::tuple<bool, bool, int, int, int, int, int>> {};
267
TEST_P(Conv2D,Op)268 TEST_P(Conv2D, Op)
269 {
270 test_conv(std::get<6>(GetParam()),
271 std::get<5>(GetParam()),
272 std::get<4>(GetParam()),
273 std::get<3>(GetParam()),
274 std::get<2>(GetParam()),
275 std::get<1>(GetParam()),
276 std::get<0>(GetParam()),
277 false, /* depthwise */
278 4);
279 }
280
281 static inline std::string
Conv2DTestCaseName(const testing::TestParamInfo<std::tuple<bool,bool,int,int,int,int,int>> & info)282 Conv2DTestCaseName(
283 const testing::TestParamInfo<std::tuple<bool, bool, int, int, int, int, int>> &info)
284 {
285 std::string name = "";
286
287 name += "input_size_" + std::to_string(std::get<6>(info.param));
288 name += "_weight_size_" + std::to_string(std::get<5>(info.param));
289 name += "_input_channels_" + std::to_string(std::get<4>(info.param));
290 name += "_output_channels_" + std::to_string(std::get<3>(info.param));
291 name += "_stride_" + std::to_string(std::get<2>(info.param));
292 name += "_padding_same_" + std::to_string(std::get<1>(info.param));
293 name += "_is_signed_" + std::to_string(std::get<0>(info.param));
294
295 return name;
296 }
297
298 INSTANTIATE_TEST_SUITE_P(
299 , Conv2D,
300 ::testing::Combine(::testing::ValuesIn(is_signed),
301 ::testing::ValuesIn(padding_same),
302 ::testing::ValuesIn(stride),
303 ::testing::ValuesIn(output_channels),
304 ::testing::ValuesIn(input_channels),
305 ::testing::ValuesIn(weight_size),
306 ::testing::ValuesIn(input_size)),
307 Conv2DTestCaseName);
308
309 #endif
310
311 #if TEST_DEPTHWISE
312
313 class DepthwiseConv2D : public testing::TestWithParam<std::tuple<bool, bool, int, int, int, int>> {};
314
TEST_P(DepthwiseConv2D,Op)315 TEST_P(DepthwiseConv2D, Op)
316 {
317 test_conv(std::get<5>(GetParam()),
318 std::get<4>(GetParam()),
319 std::get<3>(GetParam()),
320 std::get<3>(GetParam()),
321 std::get<2>(GetParam()),
322 std::get<1>(GetParam()),
323 std::get<0>(GetParam()),
324 true, /* depthwise */
325 4);
326 }
327
328 static inline std::string
DepthwiseConv2DTestCaseName(const testing::TestParamInfo<std::tuple<bool,bool,int,int,int,int>> & info)329 DepthwiseConv2DTestCaseName(
330 const testing::TestParamInfo<std::tuple<bool, bool, int, int, int, int>> &info)
331 {
332 std::string name = "";
333
334 name += "input_size_" + std::to_string(std::get<5>(info.param));
335 name += "_weight_size_" + std::to_string(std::get<4>(info.param));
336 name += "_channels_" + std::to_string(std::get<3>(info.param));
337 name += "_stride_" + std::to_string(std::get<2>(info.param));
338 name += "_padding_same_" + std::to_string(std::get<1>(info.param));
339 name += "_is_signed_" + std::to_string(std::get<0>(info.param));
340
341 return name;
342 }
343
344 INSTANTIATE_TEST_SUITE_P(
345 , DepthwiseConv2D,
346 ::testing::Combine(::testing::ValuesIn(is_signed),
347 ::testing::ValuesIn(padding_same),
348 ::testing::ValuesIn(stride),
349 ::testing::ValuesIn(dw_channels),
350 ::testing::ValuesIn(dw_weight_size),
351 ::testing::ValuesIn(input_size)),
352 DepthwiseConv2DTestCaseName);
353
354 #endif
355
356 #if TEST_ADD
357
358 class Add : public testing::TestWithParam<std::tuple<bool, bool, int, int, int, int, int>> {};
359
TEST_P(Add,Op)360 TEST_P(Add, Op)
361 {
362 test_add(std::get<6>(GetParam()),
363 std::get<5>(GetParam()),
364 std::get<4>(GetParam()),
365 std::get<3>(GetParam()),
366 std::get<2>(GetParam()),
367 std::get<1>(GetParam()),
368 std::get<0>(GetParam()),
369 false, /* depthwise */
370 4,
371 TOLERANCE);
372 }
373
374 static inline std::string
AddTestCaseName(const testing::TestParamInfo<std::tuple<bool,bool,int,int,int,int,int>> & info)375 AddTestCaseName(
376 const testing::TestParamInfo<std::tuple<bool, bool, int, int, int, int, int>> &info)
377 {
378 std::string name = "";
379
380 name += "input_size_" + std::to_string(std::get<6>(info.param));
381 name += "_weight_size_" + std::to_string(std::get<5>(info.param));
382 name += "_input_channels_" + std::to_string(std::get<4>(info.param));
383 name += "_output_channels_" + std::to_string(std::get<3>(info.param));
384 name += "_stride_" + std::to_string(std::get<2>(info.param));
385 name += "_padding_same_" + std::to_string(std::get<1>(info.param));
386 name += "_is_signed_" + std::to_string(std::get<0>(info.param));
387
388 return name;
389 }
390
391 INSTANTIATE_TEST_SUITE_P(
392 , Add,
393 ::testing::Combine(::testing::ValuesIn(is_signed),
394 ::testing::ValuesIn(padding_same),
395 ::testing::ValuesIn(stride),
396 ::testing::ValuesIn(output_channels),
397 ::testing::ValuesIn(input_channels),
398 ::testing::ValuesIn(weight_size),
399 ::testing::ValuesIn(input_size)),
400 AddTestCaseName);
401
402 class AddQuant : public testing::TestWithParam<int> {};
403
TEST_P(AddQuant,Op)404 TEST_P(AddQuant, Op)
405 {
406 test_add(40,
407 1,
408 1,
409 1,
410 1,
411 false, /* padding_same */
412 false, /* is_signed */
413 false, /* depthwise */
414 GetParam(),
415 QUANT_TOLERANCE);
416 }
417
418 INSTANTIATE_TEST_SUITE_P(
419 , AddQuant,
420 ::testing::Range(0, 100));
421
422 #endif
423
424 #if TEST_FULLY_CONNECTED
425
426 class FullyConnected : public testing::TestWithParam<std::tuple<bool, int, int>> {};
427
TEST_P(FullyConnected,Op)428 TEST_P(FullyConnected, Op)
429 {
430 test_fully_connected(
431 std::get<2>(GetParam()),
432 std::get<1>(GetParam()),
433 std::get<0>(GetParam()),
434 4);
435 }
436
437 static inline std::string
FullyConnectedTestCaseName(const testing::TestParamInfo<std::tuple<bool,int,int>> & info)438 FullyConnectedTestCaseName(
439 const testing::TestParamInfo<std::tuple<bool, int, int>> &info)
440 {
441 std::string name = "";
442
443 name += "input_size_" + std::to_string(std::get<2>(info.param));
444 name += "_output_channels_" + std::to_string(std::get<1>(info.param));
445 name += "_is_signed_" + std::to_string(std::get<0>(info.param));
446
447 return name;
448 }
449
450 INSTANTIATE_TEST_SUITE_P(
451 , FullyConnected,
452 ::testing::Combine(::testing::ValuesIn(is_signed),
453 ::testing::ValuesIn(output_channels),
454 ::testing::ValuesIn(fc_size)),
455 FullyConnectedTestCaseName);
456
457 #endif
458
459 #if TEST_MOBILENETV1
460
461 class MobileNetV1 : public ::testing::Test {};
462
463 class MobileNetV1Param : public testing::TestWithParam<int> {};
464
TEST(MobileNetV1,Whole)465 TEST(MobileNetV1, Whole)
466 {
467 std::ostringstream file_path;
468 assert(getenv("TEFLON_TEST_DATA"));
469 file_path << getenv("TEFLON_TEST_DATA") << "/mobilenet_v1_1.0_224_quant.tflite";
470
471 test_model_file(file_path.str(), MODEL_TOLERANCE, true);
472 }
473
TEST_P(MobileNetV1Param,Op)474 TEST_P(MobileNetV1Param, Op)
475 {
476 std::ostringstream file_path;
477 assert(getenv("TEFLON_TEST_DATA"));
478 file_path << getenv("TEFLON_TEST_DATA") << "/mb-" << std::setfill('0') << std::setw(3) << GetParam() << ".tflite";
479
480 test_model_file(file_path.str(), MODEL_TOLERANCE, true);
481 }
482
483 static inline std::string
MobileNetV1TestCaseName(const testing::TestParamInfo<int> & info)484 MobileNetV1TestCaseName(
485 const testing::TestParamInfo<int> &info)
486 {
487 std::string name = "";
488 std::string param = std::to_string(info.param);
489
490 name += "mb";
491 name += std::string(3 - param.length(), '0');
492 name += param;
493
494 return name;
495 }
496
497 INSTANTIATE_TEST_SUITE_P(
498 , MobileNetV1Param,
499 ::testing::Range(0, 31),
500 MobileNetV1TestCaseName);
501
502 #endif
503
504 #if TEST_MOBILEDET
505
506 class MobileDet : public ::testing::Test {};
507
508 class MobileDetParam : public testing::TestWithParam<int> {};
509
TEST(MobileDet,Whole)510 TEST(MobileDet, Whole)
511 {
512 std::ostringstream file_path;
513 assert(getenv("TEFLON_TEST_DATA"));
514 file_path << getenv("TEFLON_TEST_DATA") << "/ssdlite_mobiledet_coco_qat_postprocess.tflite";
515
516 test_model_file(file_path.str(), MODEL_TOLERANCE, true);
517 }
518
TEST_P(MobileDetParam,Op)519 TEST_P(MobileDetParam, Op)
520 {
521 std::ostringstream file_path;
522 assert(getenv("TEFLON_TEST_DATA"));
523 file_path << getenv("TEFLON_TEST_DATA") << "/mobiledet-" << std::setfill('0') << std::setw(3) << GetParam() << ".tflite";
524
525 test_model_file(file_path.str(), MODEL_TOLERANCE, true);
526 }
527
528 static inline std::string
MobileDetTestCaseName(const testing::TestParamInfo<int> & info)529 MobileDetTestCaseName(
530 const testing::TestParamInfo<int> &info)
531 {
532 std::string name = "";
533 std::string param = std::to_string(info.param);
534
535 name += "mobiledet";
536 name += std::string(3 - param.length(), '0');
537 name += param;
538
539 return name;
540 }
541
542 INSTANTIATE_TEST_SUITE_P(
543 , MobileDetParam,
544 ::testing::Range(0, 124),
545 MobileDetTestCaseName);
546
547 #endif
548
549 #if TEST_YOLOX
550
551 class YoloX : public ::testing::Test {};
552
553 class YoloXParam : public testing::TestWithParam<int> {};
554
TEST(YoloX,Whole)555 TEST(YoloX, Whole)
556 {
557 std::ostringstream file_path;
558 assert(getenv("TEFLON_TEST_DATA"));
559 file_path << getenv("TEFLON_TEST_DATA") << "/yolox.tflite";
560
561 test_model_file(file_path.str(), YOLOX_TOLERANCE, true);
562 }
563
TEST_P(YoloXParam,Op)564 TEST_P(YoloXParam, Op)
565 {
566 std::ostringstream file_path;
567 assert(getenv("TEFLON_TEST_DATA"));
568 file_path << getenv("TEFLON_TEST_DATA") << "/yolox-" << std::setfill('0') << std::setw(3) << GetParam() << ".tflite";
569
570 test_model_file(file_path.str(), MODEL_TOLERANCE, true);
571 }
572
573 static inline std::string
YoloXTestCaseName(const testing::TestParamInfo<int> & info)574 YoloXTestCaseName(
575 const testing::TestParamInfo<int> &info)
576 {
577 std::string name = "";
578 std::string param = std::to_string(info.param);
579
580 name += "yolox";
581 name += std::string(3 - param.length(), '0');
582 name += param;
583
584 return name;
585 }
586
587 INSTANTIATE_TEST_SUITE_P(
588 , YoloXParam,
589 ::testing::Range(0, 128),
590 YoloXTestCaseName);
591
592 #endif
593
594 int
main(int argc,char ** argv)595 main(int argc, char **argv)
596 {
597 if (argc > 1 && !strcmp(argv[1], "generate_model")) {
598 void *buf = NULL;
599 size_t buf_size;
600
601 assert(argc == 11);
602
603 std::cout << "Generating model to ./model.tflite\n";
604
605 int n = 2;
606 int input_size = atoi(argv[n++]);
607 int weight_size = atoi(argv[n++]);
608 int input_channels = atoi(argv[n++]);
609 int output_channels = atoi(argv[n++]);
610 int stride = atoi(argv[n++]);
611 int padding_same = atoi(argv[n++]);
612 int is_signed = atoi(argv[n++]);
613 int depthwise = atoi(argv[n++]);
614 int seed = atoi(argv[n++]);
615
616 set_seed(seed);
617
618 buf = conv2d_generate_model(input_size, weight_size,
619 input_channels, output_channels,
620 stride, padding_same, is_signed,
621 depthwise, &buf_size);
622
623 int fd = open("model.tflite", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
624 write(fd, buf, buf_size);
625 close(fd);
626
627 return 0;
628 } else if (argc > 1 && !strcmp(argv[1], "run_model")) {
629 test_model_file(std::string(argv[2]), MODEL_TOLERANCE, false);
630 } else {
631 testing::InitGoogleTest(&argc, argv);
632 return RUN_ALL_TESTS();
633 }
634 }
635