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 "NeuralNetworksWrapper.h"
18#include "TestHarness.h"
19
20#include <gtest/gtest.h>
21
22#include <tuple>
23#include <vector>
24
25using namespace android::nn::wrapper;
26using namespace test_helper;
27
28namespace {
29
30const uint32_t INTENDED_SIZE = 3;
31const uint32_t OTHER_SIZE    = 2;
32const uint32_t UNKNOWN_SIZE  = 0;
33typedef uint8_t IntendedMatrix[INTENDED_SIZE][INTENDED_SIZE];
34
35// TODO: add a float version of this test for use against drivers that don't
36// support quantized add. b/72448000
37
38// We test three basic scenarios for each tensor dimension:
39//     INTENDED_AT_COMPILE_AND_EXECUTE: set the dimension at compile
40//     (addOperand) time to INTENDED_SIZE, use same size at execution
41//     (setInput/setOutput) time. This should always work.
42//
43//     INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE: set the dimension at compile
44//     (addOperand) time to INTENDED_SIZE, give no size at execution time.
45//     This should always work.
46//
47//     UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE: don't set the dimension at
48//     compile (addOperand) time, use INTENDED_SIZE at execute
49//     (setInput/setOutput) time. Note for constants, this just means using an
50//     unknown dimension at addOperand as there is no type parameter to
51//     setOperandValue. This should work for inputs and outputs and give an
52//     error for constants at compile time.
53//
54//     UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE: don't set the dimension at compile
55//     (addOperand) time, use OTHER_SIZE at execute (setInput/setOutput) time.
56//     This should give an error at execute time (as the constant value will
57//     have a different size).
58enum class DimensionKind { INTENDED_AT_COMPILE_AND_EXECUTE,
59                           INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE,
60                           UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE,
61                           UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE };
62typedef std::tuple<DimensionKind, DimensionKind> OperandParams;
63typedef std::tuple<OperandParams,  // first input
64                   OperandParams,  // second input
65                   OperandParams,  // constant
66                   OperandParams   // output
67                  > TestParams;
68// All relevant combinations of the basic scenarios are then created with TEST_P
69auto ioDimensionValues = testing::Values(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);
73auto constantDimensionValues = testing::Values(
74        DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE,
75        DimensionKind::UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE);
76auto ioValues = testing::Combine(ioDimensionValues, ioDimensionValues);
77auto constantValues = testing::Combine(constantDimensionValues, constantDimensionValues);
78auto combinedValues = testing::Combine(ioValues, ioValues, constantValues, ioValues);
79
80class UnknownDimensionsTest : public ::testing::TestWithParam<TestParams> {
81protected:
82    const IntendedMatrix ones = { { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 } };
83    const IntendedMatrix twos = { { 2, 2, 2 }, { 2, 2, 2 }, { 2, 2, 2 } };
84    const IntendedMatrix fives = { { 5, 5, 5 }, { 5, 5, 5 }, { 5, 5, 5 } };
85};
86
87TEST_P(UnknownDimensionsTest, UnknownDimensions) {
88    TestParams params = GetParam();
89    auto paramsForInput0 = std::get<0>(params),
90         paramsForInput1 = std::get<1>(params),
91         paramsForConst  = std::get<2>(params),
92         paramsForOutput = std::get<3>(params);
93
94    Model model;
95    std::string input0Scope("Input 0:"), input1Scope("Input 1:"),
96                constantScope("Constant:"), outputScope("Output:");
97
98    auto getDimForCompile = [](DimensionKind kind, std::string* scope) {
99        switch (kind) {
100            case DimensionKind::INTENDED_AT_COMPILE_AND_EXECUTE:
101                if (scope) scope->append(" INTENDED_AT_COMPILE_AND_EXECUTE");
102                return INTENDED_SIZE;
103            case DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE:
104                if (scope) scope->append(" INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE");
105                return INTENDED_SIZE;
106            case DimensionKind::UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE:
107                if (scope) scope->append(" UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE");
108                return UNKNOWN_SIZE;
109            case DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE:
110                if (scope) scope->append(" UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE");
111                return UNKNOWN_SIZE;
112        }
113    };
114    auto addOperand = [&model, &getDimForCompile](OperandParams params,
115                                                  std::string* scope = nullptr) {
116        OperandType matrixTypeWithPotentiallyUnknownDims(
117                Type::TENSOR_QUANT8_ASYMM,
118                { getDimForCompile(std::get<0>(params), scope),
119                  getDimForCompile(std::get<1>(params), scope) },
120                1.0f);
121        return model.addOperand(&matrixTypeWithPotentiallyUnknownDims);
122    };
123    auto inputOpd0 = addOperand(paramsForInput0, &input0Scope);
124    auto inputOpd1 = addOperand(paramsForInput1, &input1Scope);
125    auto intermediateOpd0 = addOperand(OperandParams{
126            // Dimensions for intermediate operand actually deduced at execution time
127            DimensionKind::UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE,
128            DimensionKind::UNKNOWN_AT_COMPILE_INTENDED_AT_EXECUTE});
129    auto constantOpd0 = addOperand(paramsForConst, &constantScope);
130    auto outputOpd0 = addOperand(paramsForOutput, &outputScope);
131
132    // Make the gtest failure easier to read, TEST_P just outputs a list of
133    // numbers
134    SCOPED_TRACE(input0Scope);
135    SCOPED_TRACE(input1Scope);
136    SCOPED_TRACE(constantScope);
137    SCOPED_TRACE(outputScope);
138
139    OperandType scalarType(Type::INT32, {});
140    int32_t activation(ANEURALNETWORKS_FUSED_NONE);
141    auto activationOpd0 = model.addOperand(&scalarType);
142
143    model.setOperandValue(activationOpd0, &activation, sizeof(activation));
144    model.setOperandValue(constantOpd0, twos, sizeof(twos));
145    model.addOperation(ANEURALNETWORKS_ADD,
146                       {inputOpd0, inputOpd1, activationOpd0},
147                       {intermediateOpd0});
148    model.addOperation(ANEURALNETWORKS_ADD,
149                       {intermediateOpd0, constantOpd0, activationOpd0},
150                       {outputOpd0});
151    model.identifyInputsAndOutputs({inputOpd0, inputOpd1}, {outputOpd0});
152    if (std::get<0>(paramsForConst) == DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE &&
153        std::get<1>(paramsForConst) == DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE) {
154        ASSERT_TRUE(model.isValid());
155        ASSERT_EQ(model.finish(), Result::NO_ERROR);
156    } else {
157        ASSERT_FALSE(model.isValid());
158        // There is no contract (yet) for specific errors in NeuralNetworks.h,
159        // so we just assert on not being successful.
160        ASSERT_NE(model.finish(), Result::NO_ERROR);
161        return;
162    }
163
164    Compilation compilation(&model);
165    ASSERT_EQ(compilation.finish(), Result::NO_ERROR);
166
167    IntendedMatrix actual = { { 10, 10, 10 }, { 10, 10, 10 }, { 10, 10, 10 } };
168    Execution execution(&compilation);
169
170    OperandType matrixTypeIntended(Type::TENSOR_QUANT8_ASYMM, {INTENDED_SIZE, INTENDED_SIZE}, 1.0f);
171    OperandType matrixTypeFirstOther(Type::TENSOR_QUANT8_ASYMM, {OTHER_SIZE, INTENDED_SIZE}, 1.0f);
172    OperandType matrixTypeSecondOther(Type::TENSOR_QUANT8_ASYMM, {INTENDED_SIZE, OTHER_SIZE}, 1.0f);
173    OperandType matrixTypeBothOther(Type::TENSOR_QUANT8_ASYMM, {OTHER_SIZE, OTHER_SIZE}, 1.0f);
174    bool allAreIntendedSizeAtExecution = true;
175
176    // Helper to return appropriate "type" parameter to setInput/setOutput based
177    // on OperandParams
178    auto typeAtSet = [&](OperandParams params) {
179        auto first = std::get<0>(params), second = std::get<1>(params);
180        if (first == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE &&
181            second == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE) {
182            allAreIntendedSizeAtExecution = false;
183            return &matrixTypeBothOther.operandType;
184        } else if (first == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE) {
185            allAreIntendedSizeAtExecution = false;
186            return &matrixTypeFirstOther.operandType;
187        } else if (second == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE) {
188            allAreIntendedSizeAtExecution = false;
189            return &matrixTypeSecondOther.operandType;
190        } else if (first == DimensionKind::INTENDED_AT_COMPILE_AND_EXECUTE &&
191                   second == DimensionKind::INTENDED_AT_COMPILE_AND_EXECUTE) {
192            return &matrixTypeIntended.operandType;
193        } else if (first == DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE &&
194                   second == DimensionKind::INTENDED_AT_COMPILE_NOT_SET_AT_EXECUTE) {
195            return static_cast<ANeuralNetworksOperandType*>(nullptr);
196        } else {
197            return &matrixTypeIntended.operandType;
198        }
199    };
200    // Helper to return appropriate "size" parameter to setInput/setOutput based
201    // on OperandParams
202    auto sizeAtSet = [](OperandParams params) {
203        auto first = std::get<0>(params), second = std::get<1>(params);
204        size_t firstDim = (first == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE) ?
205            OTHER_SIZE : INTENDED_SIZE;
206        size_t secondDim = (second == DimensionKind::UNKNOWN_AT_COMPILE_OTHER_AT_EXECUTE) ?
207            OTHER_SIZE : INTENDED_SIZE;
208        return firstDim * secondDim * sizeof(fives[0][0]);
209    };
210    ASSERT_EQ(execution.setInput(0, ones, sizeAtSet(paramsForInput0), typeAtSet(paramsForInput0)),
211              Result::NO_ERROR);
212    ASSERT_EQ(execution.setInput(1, twos, sizeAtSet(paramsForInput1), typeAtSet(paramsForInput1)),
213              Result::NO_ERROR);
214    ASSERT_EQ(execution.setOutput(0, actual, sizeAtSet(paramsForOutput),
215                                  typeAtSet(paramsForOutput)),
216              Result::NO_ERROR);
217
218    if (allAreIntendedSizeAtExecution) {
219        ASSERT_EQ(execution.compute(), Result::NO_ERROR);
220    } else {
221        // There is no contract (yet) for specific errors in NeuralNetworks.h,
222        // so we just assert on not being successful.
223        ASSERT_NE(execution.compute(), Result::NO_ERROR);
224        return;
225    }
226
227    using qvec = std::vector<uint8_t>;
228    constexpr size_t count = sizeof(fives) / sizeof(fives[0][0]);
229    Quant8Operands expected_opds{{0, qvec{&fives[0][0], &fives[0][0] + count}}};
230    Quant8Operands actual_opds{{0, qvec{&actual[0][0], &actual[0][0] + count}}};
231    compare(MixedTyped{ {}, {}, expected_opds }, MixedTyped{ {}, {}, actual_opds });
232}
233
234INSTANTIATE_TEST_CASE_P(UnknownCombinationsTest, UnknownDimensionsTest,
235                        combinedValues);
236
237}  // end namespace
238