1 /*
2 * Copyright (C) 2018 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 #include <gtest/gtest.h>
18
19 #include <cmath>
20 #include <string>
21 #include <tuple>
22 #include <vector>
23
24 #include "TestNeuralNetworksWrapper.h"
25
26 using namespace android::nn::test_wrapper;
27
28 namespace {
29
30 const uint32_t INTENDED_SIZE = 3;
31 const uint32_t OTHER_SIZE = 2;
32 const uint32_t UNKNOWN_SIZE = 0;
33
34 // We test three basic scenarios for each tensor dimension:
35 // INTENDED_AT_COMPILE_AND_EXECUTE: set the dimension at compile
36 // (addOperand) time to INTENDED_SIZE, use same size at execution
37 // (setInput/setOutput) time. This should always work.
38 //
39 // INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE: set the dimension at compile
40 // (addOperand) time to INTENDED_SIZE, give no size at execution time.
41 // This should always work.
42 //
43 // UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE: don't set the dimension at
44 // compile (addOperand) time, use INTENDED_SIZE at execute
45 // (setInput/setOutput) time. Note for constants, this just means using an
46 // unknown dimension at addOperand as there is no type parameter to
47 // setOperandValue. This should work for inputs and outputs and give an
48 // error for constants at compile time.
49 //
50 // UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE: don't set the dimension at compile
51 // (addOperand) time, use OTHER_SIZE at execute (setInput/setOutput) time.
52 // This should give an error at execute time (as the constant value will
53 // have a different size).
54 //
55 // All relevant combinations of the basic scenarios are then iterated over in
56 // TestAll. Note that we don't want to just use googletest's parametrized tests (TEST_P) as
57 // the 16k combinations generated too many lines of output for the test
58 // infrastructure to handle correctly. However, running all 16k in one test
59 // makes the ASAN version take so long that the automatic test runner things the
60 // command has become unresponsinve, so we split on the first level.
61 enum class DimensionKind {
62 INTENDED_AT_COMPILE_AND_EXECUTE,
63 INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE,
64 UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE,
65 UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE
66 };
67 typedef std::tuple<DimensionKind, DimensionKind> OperandParams;
68 std::vector<DimensionKind> ioDimensionValues = {
69 DimensionKind::INTENDED_AT_COMPILE_AND_EXECUTE,
70 DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE,
71 DimensionKind::UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE,
72 DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE};
73 std::vector<DimensionKind> constantDimensionValues = {
74 DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE,
75 DimensionKind::UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE};
76 std::vector<OperandParams> Combine(const std::vector<DimensionKind>& firsts,
77 const std::vector<DimensionKind>& seconds);
78 auto ioValues = Combine(ioDimensionValues, ioDimensionValues);
79 auto constantValues = Combine(constantDimensionValues, constantDimensionValues);
80
81 class UnknownDimensionsTest : public ::testing::TestWithParam<OperandParams> {
82 protected:
83 template <class T, Type TensorType>
84 void TestOne(const OperandParams& paramsForInput0, const OperandParams& paramsForInput1,
85 const OperandParams& paramsForConst, const OperandParams& paramsForOutput);
86 template <class T, Type TensorType>
87 void TestAll();
88
89 template <typename T>
90 void CompareResults(const std::vector<T>& expected, const std::vector<T>& actual);
91 };
92
93 template <typename T>
CompareGeneric(const std::vector<T> & golden,const std::vector<T> & test,std::function<void (T,T)> cmp)94 void CompareGeneric(const std::vector<T>& golden, const std::vector<T>& test,
95 std::function<void(T, T)> cmp) {
96 ASSERT_EQ(golden.size(), test.size());
97 for (uint32_t i = 0; i < golden.size(); i++) {
98 SCOPED_TRACE(testing::Message() << "When comparing element " << i);
99 cmp(golden[i], test[i]);
100 }
101 }
102
103 constexpr size_t gMaximumNumberOfErrorMessages = 10;
104
105 template <>
CompareResults(const std::vector<float> & golden,const std::vector<float> & test)106 void UnknownDimensionsTest::CompareResults<float>(const std::vector<float>& golden,
107 const std::vector<float>& test) {
108 size_t totalNumberOfErrors = 0;
109 float fpAtol = 1e-5f, fpRtol = 1e-5f;
110 CompareGeneric<float>(golden, test,
111 [&totalNumberOfErrors, fpAtol, fpRtol](float expected, float actual) {
112 // Compute the range based on both absolute tolerance and relative
113 // tolerance
114 float fpRange = fpAtol + fpRtol * std::abs(expected);
115 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
116 EXPECT_NEAR(expected, actual, fpRange);
117 }
118 if (std::abs(expected - actual) > fpRange) {
119 totalNumberOfErrors++;
120 }
121 });
122 EXPECT_EQ(size_t{0}, totalNumberOfErrors);
123 }
124
125 template <>
CompareResults(const std::vector<uint8_t> & golden,const std::vector<uint8_t> & test)126 void UnknownDimensionsTest::CompareResults<uint8_t>(const std::vector<uint8_t>& golden,
127 const std::vector<uint8_t>& test) {
128 size_t totalNumberOfErrors = 0;
129 CompareGeneric<uint8_t>(golden, test, [&totalNumberOfErrors](uint8_t expected, uint8_t actual) {
130 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
131 EXPECT_NEAR(expected, actual, 1);
132 }
133 if (std::abs(expected - actual) > 1) {
134 totalNumberOfErrors++;
135 }
136 });
137 EXPECT_EQ(size_t{0}, totalNumberOfErrors);
138 }
139
140 template <>
CompareResults(const std::vector<_Float16> & golden,const std::vector<_Float16> & test)141 void UnknownDimensionsTest::CompareResults<_Float16>(const std::vector<_Float16>& golden,
142 const std::vector<_Float16>& test) {
143 size_t totalNumberOfErrors = 0;
144 float fpAtol = 5.0f * 0.0009765625f, fpRtol = 5.0f * 0.0009765625f;
145 CompareGeneric<_Float16>(
146 golden, test,
147 [&totalNumberOfErrors, fpAtol, fpRtol](_Float16 expected, _Float16 actual) {
148 // Compute the range based on both absolute tolerance and relative
149 // tolerance
150 float fpRange = fpAtol + fpRtol * std::abs(static_cast<float>(expected));
151 if (totalNumberOfErrors < gMaximumNumberOfErrorMessages) {
152 EXPECT_NEAR(expected, actual, fpRange);
153 }
154 if (std::abs(static_cast<float>(expected - actual)) > fpRange) {
155 totalNumberOfErrors++;
156 }
157 });
158 EXPECT_EQ(size_t{0}, totalNumberOfErrors);
159 }
160
161 template <class T, Type TensorType>
TestOne(const OperandParams & paramsForInput0,const OperandParams & paramsForInput1,const OperandParams & paramsForConst,const OperandParams & paramsForOutput)162 void UnknownDimensionsTest::TestOne(const OperandParams& paramsForInput0,
163 const OperandParams& paramsForInput1,
164 const OperandParams& paramsForConst,
165 const OperandParams& paramsForOutput) {
166 typedef T IntendedMatrix[INTENDED_SIZE][INTENDED_SIZE];
167 static const IntendedMatrix ones = {{1, 1, 1}, {1, 1, 1}, {1, 1, 1}};
168 static const IntendedMatrix twos = {{2, 2, 2}, {2, 2, 2}, {2, 2, 2}};
169 static const IntendedMatrix fives = {{5, 5, 5}, {5, 5, 5}, {5, 5, 5}};
170 const float scale = TensorType == Type::TENSOR_QUANT8_ASYMM ? 1.f : 0.f;
171
172 Model model;
173 std::string input0Scope("Input 0:"), input1Scope("Input 1:"), constantScope("Constant:"),
174 outputScope("Output:");
175
176 auto getDimForCompile = [](DimensionKind kind, std::string* scope) {
177 switch (kind) {
178 case DimensionKind::INTENDED_AT_COMPILE_AND_EXECUTE:
179 if (scope) scope->append(" INTENDED_AT_COMPILE_AND_EXECUTE");
180 return INTENDED_SIZE;
181 case DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE:
182 if (scope) scope->append(" INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE");
183 return INTENDED_SIZE;
184 case DimensionKind::UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE:
185 if (scope) scope->append(" UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE");
186 return UNKNOWN_SIZE;
187 case DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE:
188 if (scope) scope->append(" UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE");
189 return UNKNOWN_SIZE;
190 }
191 };
192 auto addOperand = [&model, &getDimForCompile, scale](OperandParams params,
193 std::string* scope = nullptr) {
194 OperandType matrixTypeWithPotentiallyUnknownDims(
195 TensorType,
196 {getDimForCompile(std::get<0>(params), scope),
197 getDimForCompile(std::get<1>(params), scope)},
198 scale);
199 return model.addOperand(&matrixTypeWithPotentiallyUnknownDims);
200 };
201 auto inputOpd0 = addOperand(paramsForInput0, &input0Scope);
202 auto inputOpd1 = addOperand(paramsForInput1, &input1Scope);
203 auto intermediateOpd0 = addOperand(OperandParams{
204 // Dimensions for intermediate operand actually deduced at execution time
205 DimensionKind::UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE,
206 DimensionKind::UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE});
207 auto constantOpd0 = addOperand(paramsForConst, &constantScope);
208 auto outputOpd0 = addOperand(paramsForOutput, &outputScope);
209
210 // Make the gtest failure easier to read
211 SCOPED_TRACE(input0Scope);
212 SCOPED_TRACE(input1Scope);
213 SCOPED_TRACE(constantScope);
214 SCOPED_TRACE(outputScope);
215
216 OperandType scalarType(Type::INT32, {});
217 int32_t activation(ANEURALNETWORKS_FUSED_NONE);
218 auto activationOpd0 = model.addOperand(&scalarType);
219
220 model.setOperandValue(activationOpd0, &activation, sizeof(activation));
221 model.setOperandValue(constantOpd0, twos, sizeof(twos));
222 model.addOperation(ANEURALNETWORKS_ADD, {inputOpd0, inputOpd1, activationOpd0},
223 {intermediateOpd0});
224 model.addOperation(ANEURALNETWORKS_ADD, {intermediateOpd0, constantOpd0, activationOpd0},
225 {outputOpd0});
226 model.identifyInputsAndOutputs({inputOpd0, inputOpd1}, {outputOpd0});
227 if (std::get<0>(paramsForConst) == DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE &&
228 std::get<1>(paramsForConst) == DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE) {
229 ASSERT_TRUE(model.isValid());
230 ASSERT_EQ(model.finish(), Result::NO_ERROR);
231 } else {
232 ASSERT_FALSE(model.isValid());
233 // There is no contract (yet) for specific errors in NeuralNetworks.h,
234 // so we just assert on not being successful.
235 ASSERT_NE(model.finish(), Result::NO_ERROR);
236 return;
237 }
238
239 Compilation compilation(&model);
240 ASSERT_EQ(compilation.finish(), Result::NO_ERROR);
241
242 IntendedMatrix actual = {{10, 10, 10}, {10, 10, 10}, {10, 10, 10}};
243 Execution execution(&compilation);
244
245 OperandType matrixTypeIntended(TensorType, {INTENDED_SIZE, INTENDED_SIZE}, scale);
246 OperandType matrixTypeFirstOther(TensorType, {OTHER_SIZE, INTENDED_SIZE}, scale);
247 OperandType matrixTypeSecondOther(TensorType, {INTENDED_SIZE, OTHER_SIZE}, scale);
248 OperandType matrixTypeBothOther(TensorType, {OTHER_SIZE, OTHER_SIZE}, scale);
249 bool allAreIntendedSizeAtExecution = true;
250
251 // Helper to return appropriate "type" parameter to setInput/setOutput based
252 // on OperandParams
253 auto typeAtSet = [&](OperandParams params) {
254 auto first = std::get<0>(params), second = std::get<1>(params);
255 if (first == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE &&
256 second == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE) {
257 allAreIntendedSizeAtExecution = false;
258 return &matrixTypeBothOther.operandType;
259 } else if (first == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE) {
260 allAreIntendedSizeAtExecution = false;
261 return &matrixTypeFirstOther.operandType;
262 } else if (second == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE) {
263 allAreIntendedSizeAtExecution = false;
264 return &matrixTypeSecondOther.operandType;
265 } else if (first == DimensionKind::INTENDED_AT_COMPILE_AND_EXECUTE &&
266 second == DimensionKind::INTENDED_AT_COMPILE_AND_EXECUTE) {
267 return &matrixTypeIntended.operandType;
268 } else if (first == DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE &&
269 second == DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE) {
270 return static_cast<ANeuralNetworksOperandType*>(nullptr);
271 } else {
272 return &matrixTypeIntended.operandType;
273 }
274 };
275 // Helper to return appropriate "size" parameter to setInput/setOutput based
276 // on OperandParams
277 auto sizeAtSet = [](OperandParams params) {
278 auto first = std::get<0>(params), second = std::get<1>(params);
279 size_t firstDim = (first == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE)
280 ? OTHER_SIZE
281 : INTENDED_SIZE;
282 size_t secondDim = (second == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE)
283 ? OTHER_SIZE
284 : INTENDED_SIZE;
285 return firstDim * secondDim * sizeof(fives[0][0]);
286 };
287 ASSERT_EQ(execution.setInput(0, ones, sizeAtSet(paramsForInput0), typeAtSet(paramsForInput0)),
288 Result::NO_ERROR);
289 ASSERT_EQ(execution.setInput(1, twos, sizeAtSet(paramsForInput1), typeAtSet(paramsForInput1)),
290 Result::NO_ERROR);
291 ASSERT_EQ(
292 execution.setOutput(0, actual, sizeAtSet(paramsForOutput), typeAtSet(paramsForOutput)),
293 Result::NO_ERROR);
294
295 if (allAreIntendedSizeAtExecution) {
296 ASSERT_EQ(execution.compute(), Result::NO_ERROR);
297 } else {
298 // There is no contract (yet) for specific errors in NeuralNetworks.h,
299 // so we just assert on not being successful.
300 ASSERT_NE(execution.compute(), Result::NO_ERROR);
301 return;
302 }
303
304 constexpr size_t count = sizeof(fives) / sizeof(fives[0][0]);
305 std::vector<T> expected_opds(&fives[0][0], &fives[0][0] + count);
306 std::vector<T> actual_opds(&actual[0][0], &actual[0][0] + count);
307 CompareResults(expected_opds, actual_opds);
308 }
309
Combine(const std::vector<DimensionKind> & firsts,const std::vector<DimensionKind> & seconds)310 std::vector<OperandParams> Combine(const std::vector<DimensionKind>& firsts,
311 const std::vector<DimensionKind>& seconds) {
312 std::vector<OperandParams> ret;
313 for (auto first : firsts) {
314 for (auto second : seconds) {
315 ret.push_back({first, second});
316 }
317 }
318 return ret;
319 }
320
321 template <class T, Type TensorType>
TestAll()322 void UnknownDimensionsTest::TestAll() {
323 const OperandParams paramsForInput0 = GetParam();
324 for (auto paramsForInput1 : ioValues) {
325 for (auto paramsForConst : constantValues) {
326 for (auto paramsForOutput : ioValues) {
327 TestOne<T, TensorType>(paramsForInput0, paramsForInput1, paramsForConst,
328 paramsForOutput);
329 }
330 }
331 }
332 }
333
TEST_P(UnknownDimensionsTest,Float)334 TEST_P(UnknownDimensionsTest, Float) {
335 TestAll<float, Type::TENSOR_FLOAT32>();
336 }
337
TEST_P(UnknownDimensionsTest,Quantized)338 TEST_P(UnknownDimensionsTest, Quantized) {
339 TestAll<uint8_t, Type::TENSOR_QUANT8_ASYMM>();
340 }
341
TEST_P(UnknownDimensionsTest,Float16)342 TEST_P(UnknownDimensionsTest, Float16) {
343 TestAll<_Float16, Type::TENSOR_FLOAT16>();
344 }
345
346 INSTANTIATE_TEST_CASE_P(UnknownCombinationsTest, UnknownDimensionsTest,
347 ::testing::ValuesIn(ioValues));
348 } // end namespace
349