1 /*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 /* Header-only library for various helpers of test harness
18 * See frameworks/ml/nn/runtime/test/TestGenerated.cpp for how this is used.
19 */
20 #ifndef ANDROID_ML_NN_TOOLS_TEST_GENERATOR_TEST_HARNESS_H
21 #define ANDROID_ML_NN_TOOLS_TEST_GENERATOR_TEST_HARNESS_H
22
23 #include <gmock/gmock-matchers.h>
24 #include <gtest/gtest.h>
25
26 #include <cmath>
27 #include <functional>
28 #include <map>
29 #include <tuple>
30 #include <vector>
31
32 namespace test_helper {
33
34 constexpr const size_t gMaximumNumberOfErrorMessages = 10;
35
36 // TODO: Figure out the build dependency to make including "CpuOperationUtils.h" work.
convertFloat16ToFloat32(const _Float16 * input,std::vector<float> * output)37 inline void convertFloat16ToFloat32(const _Float16* input, std::vector<float>* output) {
38 for (size_t i = 0; i < output->size(); ++i) {
39 (*output)[i] = static_cast<float>(input[i]);
40 }
41 }
42
43 // This class is a workaround for two issues our code relies on:
44 // 1. sizeof(bool) is implementation defined.
45 // 2. vector<bool> does not allow direct pointer access via the data() method.
46 class bool8 {
47 public:
bool8()48 bool8() : mValue() {}
bool8(bool value)49 /* implicit */ bool8(bool value) : mValue(value) {}
50 inline operator bool() const { return mValue != 0; }
51
52 private:
53 uint8_t mValue;
54 };
55
56 static_assert(sizeof(bool8) == 1, "size of bool8 must be 8 bits");
57
58 typedef std::map<int, std::vector<uint32_t>> OperandDimensions;
59 typedef std::map<int, std::vector<float>> Float32Operands;
60 typedef std::map<int, std::vector<int32_t>> Int32Operands;
61 typedef std::map<int, std::vector<uint8_t>> Quant8AsymmOperands;
62 typedef std::map<int, std::vector<int16_t>> Quant16SymmOperands;
63 typedef std::map<int, std::vector<_Float16>> Float16Operands;
64 typedef std::map<int, std::vector<bool8>> Bool8Operands;
65 typedef std::map<int, std::vector<int8_t>> Quant8ChannelOperands;
66 typedef std::map<int, std::vector<uint16_t>> Quant16AsymmOperands;
67 typedef std::map<int, std::vector<int8_t>> Quant8SymmOperands;
68 struct MixedTyped {
69 static constexpr size_t kNumTypes = 9;
70 OperandDimensions operandDimensions;
71 Float32Operands float32Operands;
72 Int32Operands int32Operands;
73 Quant8AsymmOperands quant8AsymmOperands;
74 Quant16SymmOperands quant16SymmOperands;
75 Float16Operands float16Operands;
76 Bool8Operands bool8Operands;
77 Quant8ChannelOperands quant8ChannelOperands;
78 Quant16AsymmOperands quant16AsymmOperands;
79 Quant8SymmOperands quant8SymmOperands;
80 };
81 typedef std::pair<MixedTyped, MixedTyped> MixedTypedExampleType;
82
83 // Mixed-typed examples
84 typedef struct {
85 MixedTypedExampleType operands;
86 // Specifies the RANDOM_MULTINOMIAL distribution tolerance.
87 // If set to greater than zero, the input is compared as log-probabilities
88 // to the output and must be within this tolerance to pass.
89 float expectedMultinomialDistributionTolerance = 0.0;
90 } MixedTypedExample;
91
92 // Go through all index-value pairs of a given input type
93 template <typename T>
for_each(const std::map<int,std::vector<T>> & idx_and_data,std::function<void (int,const std::vector<T> &)> execute)94 inline void for_each(const std::map<int, std::vector<T>>& idx_and_data,
95 std::function<void(int, const std::vector<T>&)> execute) {
96 for (auto& i : idx_and_data) {
97 execute(i.first, i.second);
98 }
99 }
100
101 // non-const variant of for_each
102 template <typename T>
for_each(std::map<int,std::vector<T>> & idx_and_data,std::function<void (int,std::vector<T> &)> execute)103 inline void for_each(std::map<int, std::vector<T>>& idx_and_data,
104 std::function<void(int, std::vector<T>&)> execute) {
105 for (auto& i : idx_and_data) {
106 execute(i.first, i.second);
107 }
108 }
109
110 // Go through all index-value pairs of a given input type
111 template <typename T>
for_each(const std::map<int,std::vector<T>> & golden,std::map<int,std::vector<T>> & test,std::function<void (int,const std::vector<T> &,std::vector<T> &)> execute)112 inline void for_each(const std::map<int, std::vector<T>>& golden,
113 std::map<int, std::vector<T>>& test,
114 std::function<void(int, const std::vector<T>&, std::vector<T>&)> execute) {
115 for_each<T>(golden, [&test, &execute](int index, const std::vector<T>& g) {
116 auto& t = test[index];
117 execute(index, g, t);
118 });
119 }
120
121 // Go through all index-value pairs of a given input type
122 template <typename T>
for_each(const std::map<int,std::vector<T>> & golden,const std::map<int,std::vector<T>> & test,std::function<void (int,const std::vector<T> &,const std::vector<T> &)> execute)123 inline void for_each(
124 const std::map<int, std::vector<T>>& golden, const std::map<int, std::vector<T>>& test,
125 std::function<void(int, const std::vector<T>&, const std::vector<T>&)> execute) {
126 for_each<T>(golden, [&test, &execute](int index, const std::vector<T>& g) {
127 auto t = test.find(index);
128 ASSERT_NE(t, test.end());
129 execute(index, g, t->second);
130 });
131 }
132
133 // internal helper for for_all
134 template <typename T>
for_all_internal(std::map<int,std::vector<T>> & idx_and_data,std::function<void (int,void *,size_t)> execute_this)135 inline void for_all_internal(std::map<int, std::vector<T>>& idx_and_data,
136 std::function<void(int, void*, size_t)> execute_this) {
137 for_each<T>(idx_and_data, [&execute_this](int idx, std::vector<T>& m) {
138 execute_this(idx, static_cast<void*>(m.data()), m.size() * sizeof(T));
139 });
140 }
141
142 // Go through all index-value pairs of all input types
143 // expects a functor that takes (int index, void *raw data, size_t sz)
for_all(MixedTyped & idx_and_data,std::function<void (int,void *,size_t)> execute_this)144 inline void for_all(MixedTyped& idx_and_data,
145 std::function<void(int, void*, size_t)> execute_this) {
146 for_all_internal(idx_and_data.float32Operands, execute_this);
147 for_all_internal(idx_and_data.int32Operands, execute_this);
148 for_all_internal(idx_and_data.quant8AsymmOperands, execute_this);
149 for_all_internal(idx_and_data.quant16SymmOperands, execute_this);
150 for_all_internal(idx_and_data.float16Operands, execute_this);
151 for_all_internal(idx_and_data.bool8Operands, execute_this);
152 for_all_internal(idx_and_data.quant8ChannelOperands, execute_this);
153 for_all_internal(idx_and_data.quant16AsymmOperands, execute_this);
154 for_all_internal(idx_and_data.quant8SymmOperands, execute_this);
155 static_assert(9 == MixedTyped::kNumTypes,
156 "Number of types in MixedTyped changed, but for_all function wasn't updated");
157 }
158
159 // Const variant of internal helper for for_all
160 template <typename T>
for_all_internal(const std::map<int,std::vector<T>> & idx_and_data,std::function<void (int,const void *,size_t)> execute_this)161 inline void for_all_internal(const std::map<int, std::vector<T>>& idx_and_data,
162 std::function<void(int, const void*, size_t)> execute_this) {
163 for_each<T>(idx_and_data, [&execute_this](int idx, const std::vector<T>& m) {
164 execute_this(idx, static_cast<const void*>(m.data()), m.size() * sizeof(T));
165 });
166 }
167
168 // Go through all index-value pairs (const variant)
169 // expects a functor that takes (int index, const void *raw data, size_t sz)
for_all(const MixedTyped & idx_and_data,std::function<void (int,const void *,size_t)> execute_this)170 inline void for_all(const MixedTyped& idx_and_data,
171 std::function<void(int, const void*, size_t)> execute_this) {
172 for_all_internal(idx_and_data.float32Operands, execute_this);
173 for_all_internal(idx_and_data.int32Operands, execute_this);
174 for_all_internal(idx_and_data.quant8AsymmOperands, execute_this);
175 for_all_internal(idx_and_data.quant16SymmOperands, execute_this);
176 for_all_internal(idx_and_data.float16Operands, execute_this);
177 for_all_internal(idx_and_data.bool8Operands, execute_this);
178 for_all_internal(idx_and_data.quant8ChannelOperands, execute_this);
179 for_all_internal(idx_and_data.quant16AsymmOperands, execute_this);
180 for_all_internal(idx_and_data.quant8SymmOperands, execute_this);
181 static_assert(
182 9 == MixedTyped::kNumTypes,
183 "Number of types in MixedTyped changed, but const for_all function wasn't updated");
184 }
185
186 // Helper template - resize test output per golden
187 template <typename T>
resize_accordingly_(const std::map<int,std::vector<T>> & golden,std::map<int,std::vector<T>> & test)188 inline void resize_accordingly_(const std::map<int, std::vector<T>>& golden,
189 std::map<int, std::vector<T>>& test) {
190 for_each<T>(golden, test,
191 [](int, const std::vector<T>& g, std::vector<T>& t) { t.resize(g.size()); });
192 }
193
194 template <>
195 inline void resize_accordingly_<uint32_t>(const OperandDimensions& golden,
196 OperandDimensions& test) {
197 for_each<uint32_t>(
198 golden, test,
199 [](int, const std::vector<uint32_t>& g, std::vector<uint32_t>& t) { t = g; });
200 }
201
resize_accordingly(const MixedTyped & golden,MixedTyped & test)202 inline void resize_accordingly(const MixedTyped& golden, MixedTyped& test) {
203 resize_accordingly_(golden.operandDimensions, test.operandDimensions);
204 resize_accordingly_(golden.float32Operands, test.float32Operands);
205 resize_accordingly_(golden.int32Operands, test.int32Operands);
206 resize_accordingly_(golden.quant8AsymmOperands, test.quant8AsymmOperands);
207 resize_accordingly_(golden.quant16SymmOperands, test.quant16SymmOperands);
208 resize_accordingly_(golden.float16Operands, test.float16Operands);
209 resize_accordingly_(golden.bool8Operands, test.bool8Operands);
210 resize_accordingly_(golden.quant8ChannelOperands, test.quant8ChannelOperands);
211 resize_accordingly_(golden.quant16AsymmOperands, test.quant16AsymmOperands);
212 resize_accordingly_(golden.quant8SymmOperands, test.quant8SymmOperands);
213 static_assert(9 == MixedTyped::kNumTypes,
214 "Number of types in MixedTyped changed, but resize_accordingly function wasn't "
215 "updated");
216 }
217
218 template <typename T>
filter_internal(const std::map<int,std::vector<T>> & golden,std::map<int,std::vector<T>> * filtered,std::function<bool (int)> is_ignored)219 void filter_internal(const std::map<int, std::vector<T>>& golden,
220 std::map<int, std::vector<T>>* filtered, std::function<bool(int)> is_ignored) {
221 for_each<T>(golden, [filtered, &is_ignored](int index, const std::vector<T>& m) {
222 auto& g = *filtered;
223 if (!is_ignored(index)) g[index] = m;
224 });
225 }
226
filter(const MixedTyped & golden,std::function<bool (int)> is_ignored)227 inline MixedTyped filter(const MixedTyped& golden,
228 std::function<bool(int)> is_ignored) {
229 MixedTyped filtered;
230 filter_internal(golden.operandDimensions, &filtered.operandDimensions, is_ignored);
231 filter_internal(golden.float32Operands, &filtered.float32Operands, is_ignored);
232 filter_internal(golden.int32Operands, &filtered.int32Operands, is_ignored);
233 filter_internal(golden.quant8AsymmOperands, &filtered.quant8AsymmOperands, is_ignored);
234 filter_internal(golden.quant16SymmOperands, &filtered.quant16SymmOperands, is_ignored);
235 filter_internal(golden.float16Operands, &filtered.float16Operands, is_ignored);
236 filter_internal(golden.bool8Operands, &filtered.bool8Operands, is_ignored);
237 filter_internal(golden.quant8ChannelOperands, &filtered.quant8ChannelOperands, is_ignored);
238 filter_internal(golden.quant16AsymmOperands, &filtered.quant16AsymmOperands, is_ignored);
239 filter_internal(golden.quant8SymmOperands, &filtered.quant8SymmOperands, is_ignored);
240 static_assert(9 == MixedTyped::kNumTypes,
241 "Number of types in MixedTyped changed, but compare function wasn't updated");
242 return filtered;
243 }
244
245 // Compare results
246 template <typename T>
compare_(const std::map<int,std::vector<T>> & golden,const std::map<int,std::vector<T>> & test,std::function<void (T,T)> cmp)247 void compare_(const std::map<int, std::vector<T>>& golden,
248 const std::map<int, std::vector<T>>& test, std::function<void(T, T)> cmp) {
249 for_each<T>(golden, test, [&cmp](int index, const std::vector<T>& g, const std::vector<T>& t) {
250 for (unsigned int i = 0; i < g.size(); i++) {
251 SCOPED_TRACE(testing::Message()
252 << "When comparing output " << index << " element " << i);
253 cmp(g[i], t[i]);
254 }
255 });
256 }
257
258 // TODO: Allow passing accuracy criteria from spec.
259 // Currently we only need relaxed accuracy criteria on mobilenet tests, so we return the quant8
260 // tolerance simply based on the current test name.
getQuant8AllowedError()261 inline int getQuant8AllowedError() {
262 const ::testing::TestInfo* const testInfo =
263 ::testing::UnitTest::GetInstance()->current_test_info();
264 const std::string testCaseName = testInfo->test_case_name();
265 const std::string testName = testInfo->name();
266 // We relax the quant8 precision for all tests with mobilenet:
267 // - CTS/VTS GeneratedTest and DynamicOutputShapeTest with mobilenet
268 // - VTS CompilationCachingTest and CompilationCachingSecurityTest except for TOCTOU tests
269 if (testName.find("mobilenet") != std::string::npos ||
270 (testCaseName.find("CompilationCaching") != std::string::npos &&
271 testName.find("TOCTOU") == std::string::npos)) {
272 return 2;
273 } else {
274 return 1;
275 }
276 }
277
278 inline void compare(const MixedTyped& golden, const MixedTyped& test,
279 float fpAtol = 1e-5f, float fpRtol = 1e-5f) {
280 int quant8AllowedError = getQuant8AllowedError();
281 for_each<uint32_t>(
282 golden.operandDimensions, test.operandDimensions,
283 [](int index, const std::vector<uint32_t>& g, const std::vector<uint32_t>& t) {
284 SCOPED_TRACE(testing::Message()
285 << "When comparing dimensions for output " << index);
286 EXPECT_EQ(g, t);
287 });
288 size_t totalNumberOfErrors = 0;
289 compare_<float>(golden.float32Operands, test.float32Operands,
290 [&totalNumberOfErrors, fpAtol, fpRtol](float expected, float actual) {
291 // Compute the range based on both absolute tolerance and relative tolerance
292 float fpRange = fpAtol + fpRtol * std::abs(expected);
293 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
294 EXPECT_NEAR(expected, actual, fpRange);
295 }
296 if (std::abs(expected - actual) > fpRange) {
297 totalNumberOfErrors++;
298 }
299 });
300 compare_<int32_t>(golden.int32Operands, test.int32Operands,
301 [&totalNumberOfErrors](int32_t expected, int32_t actual) {
302 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
303 EXPECT_EQ(expected, actual);
304 }
305 if (expected != actual) {
306 totalNumberOfErrors++;
307 }
308 });
309 compare_<uint8_t>(golden.quant8AsymmOperands, test.quant8AsymmOperands,
310 [&totalNumberOfErrors, quant8AllowedError](uint8_t expected, uint8_t actual) {
311 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
312 EXPECT_NEAR(expected, actual, quant8AllowedError);
313 }
314 if (std::abs(expected - actual) > quant8AllowedError) {
315 totalNumberOfErrors++;
316 }
317 });
318 compare_<int16_t>(golden.quant16SymmOperands, test.quant16SymmOperands,
319 [&totalNumberOfErrors](int16_t expected, int16_t actual) {
320 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
321 EXPECT_NEAR(expected, actual, 1);
322 }
323 if (std::abs(expected - actual) > 1) {
324 totalNumberOfErrors++;
325 }
326 });
327 compare_<_Float16>(golden.float16Operands, test.float16Operands,
328 [&totalNumberOfErrors, fpAtol, fpRtol](_Float16 expected, _Float16 actual) {
329 // Compute the range based on both absolute tolerance and relative
330 // tolerance
331 float fpRange = fpAtol + fpRtol * std::abs(static_cast<float>(expected));
332 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
333 EXPECT_NEAR(expected, actual, fpRange);
334 }
335 if (std::abs(static_cast<float>(expected - actual)) > fpRange) {
336 totalNumberOfErrors++;
337 }
338 });
339 compare_<bool8>(golden.bool8Operands, test.bool8Operands,
340 [&totalNumberOfErrors](bool expected, bool actual) {
341 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
342 EXPECT_EQ(expected, actual);
343 }
344 if (expected != actual) {
345 totalNumberOfErrors++;
346 }
347 });
348 compare_<int8_t>(golden.quant8ChannelOperands, test.quant8ChannelOperands,
349 [&totalNumberOfErrors, &quant8AllowedError](int8_t expected, int8_t actual) {
350 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
351 EXPECT_NEAR(expected, actual, quant8AllowedError);
352 }
353 if (std::abs(static_cast<int>(expected) - static_cast<int>(actual)) >
354 quant8AllowedError) {
355 totalNumberOfErrors++;
356 }
357 });
358 compare_<uint16_t>(golden.quant16AsymmOperands, test.quant16AsymmOperands,
359 [&totalNumberOfErrors](int16_t expected, int16_t actual) {
360 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
361 EXPECT_NEAR(expected, actual, 1);
362 }
363 if (std::abs(expected - actual) > 1) {
364 totalNumberOfErrors++;
365 }
366 });
367 compare_<int8_t>(golden.quant8SymmOperands, test.quant8SymmOperands,
368 [&totalNumberOfErrors, quant8AllowedError](int8_t expected, int8_t actual) {
369 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
370 EXPECT_NEAR(expected, actual, quant8AllowedError);
371 }
372 if (std::abs(static_cast<int>(expected) - static_cast<int>(actual)) >
373 quant8AllowedError) {
374 totalNumberOfErrors++;
375 }
376 });
377
378 static_assert(9 == MixedTyped::kNumTypes,
379 "Number of types in MixedTyped changed, but compare function wasn't updated");
380 EXPECT_EQ(size_t{0}, totalNumberOfErrors);
381 }
382
383 // Calculates the expected probability from the unnormalized log-probability of
384 // each class in the input and compares it to the actual ocurrence of that class
385 // in the output.
expectMultinomialDistributionWithinTolerance(const MixedTyped & test,const MixedTypedExample & example)386 inline void expectMultinomialDistributionWithinTolerance(const MixedTyped& test,
387 const MixedTypedExample& example) {
388 // TODO: These should be parameters but aren't currently preserved in the example.
389 const int kBatchSize = 1;
390 const int kNumClasses = 1024;
391 const int kNumSamples = 128;
392
393 std::vector<int32_t> output = test.int32Operands.at(0);
394 std::vector<int> class_counts;
395 class_counts.resize(kNumClasses);
396 for (int index : output) {
397 class_counts[index]++;
398 }
399 std::vector<float> input;
400 Float32Operands float32Operands = example.operands.first.float32Operands;
401 if (!float32Operands.empty()) {
402 input = example.operands.first.float32Operands.at(0);
403 } else {
404 std::vector<_Float16> inputFloat16 = example.operands.first.float16Operands.at(0);
405 input.resize(inputFloat16.size());
406 convertFloat16ToFloat32(inputFloat16.data(), &input);
407 }
408 for (int b = 0; b < kBatchSize; ++b) {
409 float probability_sum = 0;
410 const int batch_index = kBatchSize * b;
411 for (int i = 0; i < kNumClasses; ++i) {
412 probability_sum += expf(input[batch_index + i]);
413 }
414 for (int i = 0; i < kNumClasses; ++i) {
415 float probability =
416 static_cast<float>(class_counts[i]) / static_cast<float>(kNumSamples);
417 float probability_expected = expf(input[batch_index + i]) / probability_sum;
418 EXPECT_THAT(probability,
419 ::testing::FloatNear(probability_expected,
420 example.expectedMultinomialDistributionTolerance));
421 }
422 }
423 }
424
425 }; // namespace test_helper
426
427 #endif // ANDROID_ML_NN_TOOLS_TEST_GENERATOR_TEST_HARNESS_H
428