/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #define LOG_TAG "neuralnetworks_hidl_hal_test" #include #include #include "1.3/Callbacks.h" #include "1.3/Utils.h" #include "GeneratedTestHarness.h" #include "MemoryUtils.h" #include "TestHarness.h" #include "Utils.h" #include "VtsHalNeuralnetworks.h" namespace android::hardware::neuralnetworks::V1_3::vts::functional { using namespace test_helper; using implementation::ExecutionCallback; using implementation::PreparedModelCallback; using V1_0::RequestArgument; using V1_1::ExecutionPreference; using V1_2::Constant; using V1_2::MeasureTiming; using V1_2::OutputShape; using V1_2::Timing; namespace { const auto kNamedDeviceChoices = testing::ValuesIn(getNamedDevices()); // A 1.3 driver is likely to support at least one of the following operand types. const std::vector kTestOperandTypeChoicesVector = { TestOperandType::TENSOR_FLOAT32, TestOperandType::TENSOR_FLOAT16, TestOperandType::TENSOR_QUANT8_ASYMM, TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED, }; const auto kTestOperandTypeChoices = testing::ValuesIn(kTestOperandTypeChoicesVector); bool isInChoices(TestOperandType type) { return std::count(kTestOperandTypeChoicesVector.begin(), kTestOperandTypeChoicesVector.end(), type) > 0; } bool isFloat(TestOperandType type) { CHECK(isInChoices(type)); return type == TestOperandType::TENSOR_FLOAT32 || type == TestOperandType::TENSOR_FLOAT16; } // Create dummy buffers for model constants as well as inputs and outputs. // We only care about the size here because we will not check accuracy in validation tests. void createDummyData(TestModel* testModel) { for (auto& operand : testModel->main.operands) { if (operand.data != nullptr) continue; switch (operand.lifetime) { case TestOperandLifeTime::SUBGRAPH_INPUT: case TestOperandLifeTime::SUBGRAPH_OUTPUT: case TestOperandLifeTime::CONSTANT_COPY: case TestOperandLifeTime::CONSTANT_REFERENCE: { const uint32_t size = nn::nonExtensionOperandSizeOfData( static_cast(operand.type), operand.dimensions); operand.data = TestBuffer(size); } break; default: break; } } } TestOperand createInt32Scalar(int32_t value) { return { .type = TestOperandType::INT32, .dimensions = {}, .numberOfConsumers = 1, .scale = 0.0f, .zeroPoint = 0, .lifetime = TestOperandLifeTime::CONSTANT_COPY, .data = TestBuffer::createFromVector({value}), }; } // Construct a test model with multiple CONV_2D operations with the given operand as inputs. // The dimensions of the filters are chosen to ensure outputs has the same dimensions as inputs. // We choose CONV_2D operation because it is commonly supported by most drivers. TestModel createConvModel(const TestOperand& operand, uint32_t numOperations) { CHECK(isInChoices(operand.type)); TestOperand weight = {.type = operand.type, .dimensions = {operand.dimensions[3], 3, 3, operand.dimensions[3]}, .numberOfConsumers = 1, .scale = isFloat(operand.type) ? 0.0f : 1.0f, .zeroPoint = 0, .lifetime = TestOperandLifeTime::CONSTANT_COPY}; TestOperand bias = { .type = isFloat(operand.type) ? operand.type : TestOperandType::TENSOR_INT32, .dimensions = {operand.dimensions[3]}, .numberOfConsumers = 1, .scale = operand.scale * weight.scale, .zeroPoint = 0, .lifetime = TestOperandLifeTime::CONSTANT_COPY}; TestOperand output = operand; output.numberOfConsumers = 0; output.lifetime = TestOperandLifeTime::SUBGRAPH_OUTPUT; const std::vector operands = { operand, std::move(weight), std::move(bias), createInt32Scalar(1), // same padding createInt32Scalar(1), // width stride createInt32Scalar(1), // height stride createInt32Scalar(0), // activation = NONE std::move(output), }; TestModel model; for (uint32_t i = 0; i < numOperations; i++) { model.main.operands.insert(model.main.operands.end(), operands.begin(), operands.end()); const uint32_t inputIndex = operands.size() * i; const uint32_t outputIndex = inputIndex + operands.size() - 1; std::vector inputs(operands.size() - 1); std::iota(inputs.begin(), inputs.end(), inputIndex); model.main.operations.push_back({.type = TestOperationType::CONV_2D, .inputs = std::move(inputs), .outputs = {outputIndex}}); model.main.inputIndexes.push_back(inputIndex); model.main.outputIndexes.push_back(outputIndex); } createDummyData(&model); return model; } // Construct a test model with a single ADD operation with the given operand as input0 and input1. // This is to cover additional cases that the CONV_2D model does not support, e.g. arbitrary input // operand rank, scalar input operand. We choose ADD operation because it is commonly supported by // most drivers. TestModel createSingleAddModel(const TestOperand& operand) { CHECK(isInChoices(operand.type)); TestOperand act = { .type = TestOperandType::INT32, .dimensions = {}, .numberOfConsumers = 1, .scale = 0.0f, .zeroPoint = 0, .lifetime = TestOperandLifeTime::SUBGRAPH_INPUT, }; TestOperand output = operand; output.numberOfConsumers = 0; output.lifetime = TestOperandLifeTime::SUBGRAPH_OUTPUT; TestModel model = { .main = { .operands = { operand, operand, std::move(act), output, }, .operations = {{.type = TestOperationType::ADD, .inputs = {0, 1, 2}, .outputs = {3}}}, .inputIndexes = {0, 1, 2}, .outputIndexes = {3}, }, }; createDummyData(&model); return model; } // A dummy invalid IPreparedModel class for MemoryDomainAllocateTest.InvalidPreparedModel class InvalidPreparedModel : public IPreparedModel { public: Return execute(const V1_0::Request&, const sp&) override { return V1_0::ErrorStatus::GENERAL_FAILURE; } Return execute_1_2(const V1_0::Request&, V1_2::MeasureTiming, const sp&) override { return V1_0::ErrorStatus::GENERAL_FAILURE; } Return execute_1_3(const V1_3::Request&, V1_2::MeasureTiming, const V1_3::OptionalTimePoint&, const V1_3::OptionalTimeoutDuration&, const sp&) override { return V1_3::ErrorStatus::GENERAL_FAILURE; } Return executeSynchronously(const V1_0::Request&, V1_2::MeasureTiming, executeSynchronously_cb) override { return Void(); } Return executeSynchronously_1_3(const V1_3::Request&, V1_2::MeasureTiming, const V1_3::OptionalTimePoint&, const V1_3::OptionalTimeoutDuration&, executeSynchronously_1_3_cb) override { return Void(); } Return configureExecutionBurst(const sp&, const MQDescriptorSync&, const MQDescriptorSync&, configureExecutionBurst_cb) override { return Void(); } Return executeFenced(const V1_3::Request&, const hidl_vec&, V1_2::MeasureTiming, const V1_3::OptionalTimePoint&, const V1_3::OptionalTimeoutDuration&, const V1_3::OptionalTimeoutDuration&, executeFenced_cb) override { return Void(); } }; } // namespace class MemoryDomainTestBase : public testing::Test { protected: MemoryDomainTestBase(sp device, TestOperandType type) : kDevice(std::move(device)), kTestOperandType(type), kTestOperand(kTestOperandMap.at(type)), kTestOperandDataSize(nn::nonExtensionOperandSizeOfData(static_cast(type), kTestOperand.dimensions)) {} void SetUp() override { testing::Test::SetUp(); ASSERT_NE(kDevice, nullptr); const bool deviceIsResponsive = kDevice->ping().isOk(); ASSERT_TRUE(deviceIsResponsive); } sp createConvPreparedModel(const TestOperand& testOperand, uint32_t numOperations = 1) { const TestModel testModel = createConvModel(testOperand, numOperations); const Model model = createModel(testModel); sp preparedModel; createPreparedModel(kDevice, model, &preparedModel, /*reportSkipping=*/false); return preparedModel; } sp createAddPreparedModel(const TestOperand& testOperand) { const TestModel testModel = createSingleAddModel(testOperand); const Model model = createModel(testModel); sp preparedModel; createPreparedModel(kDevice, model, &preparedModel, /*reportSkipping=*/false); return preparedModel; } static const std::map kTestOperandMap; const sp kDevice; const TestOperandType kTestOperandType; const TestOperand& kTestOperand; const uint32_t kTestOperandDataSize; }; const std::map MemoryDomainTestBase::kTestOperandMap = { {TestOperandType::TENSOR_FLOAT32, { .type = TestOperandType::TENSOR_FLOAT32, .dimensions = {1, 32, 32, 8}, .numberOfConsumers = 1, .scale = 0.0f, .zeroPoint = 0, .lifetime = TestOperandLifeTime::SUBGRAPH_INPUT, }}, {TestOperandType::TENSOR_FLOAT16, { .type = TestOperandType::TENSOR_FLOAT16, .dimensions = {1, 32, 32, 8}, .numberOfConsumers = 1, .scale = 0.0f, .zeroPoint = 0, .lifetime = TestOperandLifeTime::SUBGRAPH_INPUT, }}, {TestOperandType::TENSOR_QUANT8_ASYMM, { .type = TestOperandType::TENSOR_QUANT8_ASYMM, .dimensions = {1, 32, 32, 8}, .numberOfConsumers = 1, .scale = 0.5f, .zeroPoint = 0, .lifetime = TestOperandLifeTime::SUBGRAPH_INPUT, }}, {TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED, { .type = TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED, .dimensions = {1, 32, 32, 8}, .numberOfConsumers = 1, .scale = 0.5f, .zeroPoint = 0, .lifetime = TestOperandLifeTime::SUBGRAPH_INPUT, }}, }; using MemoryDomainAllocateTestParam = std::tuple; class MemoryDomainAllocateTest : public MemoryDomainTestBase, public testing::WithParamInterface { protected: MemoryDomainAllocateTest() : MemoryDomainTestBase(getData(std::get(GetParam())), std::get(GetParam())) {} struct AllocateTestArgs { hidl_vec dimensions; hidl_vec> preparedModels; hidl_vec inputRoles; hidl_vec outputRoles; }; // Validation test for IDevice::allocate. The driver is expected to fail with INVALID_ARGUMENT, // or GENERAL_FAILURE if memory domain is not supported. void validateAllocate(AllocateTestArgs args) { const auto ret = kDevice->allocate( {.dimensions = std::move(args.dimensions)}, std::move(args.preparedModels), std::move(args.inputRoles), std::move(args.outputRoles), [](ErrorStatus status, const sp& buffer, uint32_t token) { EXPECT_TRUE(status == ErrorStatus::INVALID_ARGUMENT || status == ErrorStatus::GENERAL_FAILURE); EXPECT_EQ(buffer, nullptr); EXPECT_EQ(token, 0); }); ASSERT_TRUE(ret.isOk()); } void testConflictOperands(const sp& model1, const sp& model2) { validateAllocate({ .preparedModels = {model1, model2}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}, {.modelIndex = 1, .ioIndex = 0, .frequency = 1.0f}}, }); validateAllocate({ .preparedModels = {model1, model2}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}}, .outputRoles = {{.modelIndex = 1, .ioIndex = 0, .frequency = 1.0f}}, }); validateAllocate({ .preparedModels = {model1, model2}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}, {.modelIndex = 1, .ioIndex = 0, .frequency = 1.0f}}, }); } }; TEST_P(MemoryDomainAllocateTest, EmptyRole) { // Test with empty prepared models and roles. validateAllocate({}); auto preparedModel = createConvPreparedModel(kTestOperand); if (preparedModel == nullptr) return; // Test again with non-empty prepared models but empty roles. validateAllocate({ .preparedModels = {preparedModel}, }); } TEST_P(MemoryDomainAllocateTest, NullptrPreparedModel) { // Test with nullptr prepared model as input role. validateAllocate({ .preparedModels = {nullptr}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}}, }); // Test with nullptr prepared model as output role. validateAllocate({ .preparedModels = {nullptr}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}}, }); } TEST_P(MemoryDomainAllocateTest, InvalidPreparedModel) { sp invalidPreparedModel = new InvalidPreparedModel(); // Test with invalid prepared model as input role. validateAllocate({ .preparedModels = {invalidPreparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}}, }); // Test with invalid prepared model as output role. validateAllocate({ .preparedModels = {invalidPreparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}}, }); } TEST_P(MemoryDomainAllocateTest, InvalidModelIndex) { auto preparedModel = createConvPreparedModel(kTestOperand); if (preparedModel == nullptr) return; // This should fail, because the model index is out of bound. validateAllocate({ .preparedModels = {preparedModel}, .inputRoles = {{.modelIndex = 1, .ioIndex = 0, .frequency = 1.0f}}, }); // This should fail, because the model index is out of bound. validateAllocate({ .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 1, .ioIndex = 0, .frequency = 1.0f}}, }); } TEST_P(MemoryDomainAllocateTest, InvalidIOIndex) { auto preparedModel = createConvPreparedModel(kTestOperand); if (preparedModel == nullptr) return; // This should fail, because the model only has one input. validateAllocate({ .preparedModels = {preparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 1, .frequency = 1.0f}}, }); // This should fail, because the model only has one output. validateAllocate({ .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 1, .frequency = 1.0f}}, }); } TEST_P(MemoryDomainAllocateTest, InvalidFrequency) { auto preparedModel = createConvPreparedModel(kTestOperand); if (preparedModel == nullptr) return; for (float invalidFreq : {10.0f, 0.0f, -0.5f}) { // Test with invalid frequency for input roles. validateAllocate({ .preparedModels = {preparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = invalidFreq}}, }); // Test with invalid frequency for output roles. validateAllocate({ .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = invalidFreq}}, }); } } TEST_P(MemoryDomainAllocateTest, SameRoleSpecifiedTwice) { auto preparedModel = createConvPreparedModel(kTestOperand); if (preparedModel == nullptr) return; // Same role with same model index. validateAllocate({ .preparedModels = {preparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}, {.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}}, }); validateAllocate({ .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}, {.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}}, }); // Different model indexes, but logically referring to the same role. validateAllocate({ .preparedModels = {preparedModel, preparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}, {.modelIndex = 1, .ioIndex = 0, .frequency = 1.0f}}, }); validateAllocate({ .preparedModels = {preparedModel, preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}, {.modelIndex = 1, .ioIndex = 0, .frequency = 1.0f}}, }); } TEST_P(MemoryDomainAllocateTest, ConflictOperandType) { const std::map conflictTypeMap = { {TestOperandType::TENSOR_FLOAT32, TestOperandType::TENSOR_FLOAT16}, {TestOperandType::TENSOR_FLOAT16, TestOperandType::TENSOR_FLOAT32}, {TestOperandType::TENSOR_QUANT8_ASYMM, TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED}, {TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED, TestOperandType::TENSOR_QUANT8_ASYMM}, }; TestOperand conflictTestOperand = kTestOperand; const auto it = conflictTypeMap.find(kTestOperandType); ASSERT_FALSE(it == conflictTypeMap.end()); conflictTestOperand.type = it->second; auto preparedModel = createConvPreparedModel(kTestOperand); auto conflictPreparedModel = createConvPreparedModel(conflictTestOperand); if (preparedModel == nullptr || conflictPreparedModel == nullptr) return; testConflictOperands(preparedModel, conflictPreparedModel); } TEST_P(MemoryDomainAllocateTest, ConflictScale) { if (isFloat(kTestOperandType)) return; TestOperand conflictTestOperand = kTestOperand; ASSERT_NE(conflictTestOperand.scale, 1.0f); conflictTestOperand.scale = 1.0f; auto preparedModel = createConvPreparedModel(kTestOperand); auto conflictPreparedModel = createConvPreparedModel(conflictTestOperand); if (preparedModel == nullptr || conflictPreparedModel == nullptr) return; testConflictOperands(preparedModel, conflictPreparedModel); } TEST_P(MemoryDomainAllocateTest, ConflictZeroPoint) { if (isFloat(kTestOperandType)) return; TestOperand conflictTestOperand = kTestOperand; ASSERT_NE(conflictTestOperand.zeroPoint, 10); conflictTestOperand.zeroPoint = 10; auto preparedModel = createConvPreparedModel(kTestOperand); auto conflictPreparedModel = createConvPreparedModel(conflictTestOperand); if (preparedModel == nullptr || conflictPreparedModel == nullptr) return; testConflictOperands(preparedModel, conflictPreparedModel); } TEST_P(MemoryDomainAllocateTest, ConflictRankBetweenRoles) { TestOperand conflictTestOperand = kTestOperand; conflictTestOperand.dimensions.pop_back(); auto preparedModel = createAddPreparedModel(kTestOperand); auto conflictPreparedModel = createAddPreparedModel(conflictTestOperand); if (preparedModel == nullptr || conflictPreparedModel == nullptr) return; testConflictOperands(preparedModel, conflictPreparedModel); } TEST_P(MemoryDomainAllocateTest, ConflictDimensionsBetweenRoles) { TestOperand conflictTestOperand = kTestOperand; conflictTestOperand.dimensions[0] = 4; auto preparedModel = createConvPreparedModel(kTestOperand); auto conflictPreparedModel = createConvPreparedModel(conflictTestOperand); if (preparedModel == nullptr || conflictPreparedModel == nullptr) return; testConflictOperands(preparedModel, conflictPreparedModel); } TEST_P(MemoryDomainAllocateTest, ConflictRankBetweenRoleAndDesc) { auto preparedModel = createConvPreparedModel(kTestOperand); if (preparedModel == nullptr) return; auto badDimensions = kTestOperand.dimensions; badDimensions.pop_back(); validateAllocate({ .dimensions = badDimensions, .preparedModels = {preparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}}, }); validateAllocate({ .dimensions = badDimensions, .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}}, }); } TEST_P(MemoryDomainAllocateTest, ConflictDimensionsBetweenRoleAndDesc) { auto preparedModel = createConvPreparedModel(kTestOperand); if (preparedModel == nullptr) return; auto badDimensions = kTestOperand.dimensions; badDimensions[0] = 4; validateAllocate({ .dimensions = badDimensions, .preparedModels = {preparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}}, }); validateAllocate({ .dimensions = badDimensions, .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .frequency = 1.0f}}, }); } TEST_P(MemoryDomainAllocateTest, ConflictRankWithScalarRole) { auto preparedModel = createAddPreparedModel(kTestOperand); if (preparedModel == nullptr) return; // This should fail, because the target operand is a scalar but a non-empty dimension is // specified. validateAllocate({ .dimensions = {1}, .preparedModels = {preparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 2, .frequency = 1.0f}}, }); } std::string printMemoryDomainAllocateTest( const testing::TestParamInfo& info) { const auto& [namedDevice, operandType] = info.param; const std::string type = toString(static_cast(operandType)); return gtestCompliantName(getName(namedDevice) + "_" + type); } GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MemoryDomainAllocateTest); INSTANTIATE_TEST_SUITE_P(TestMemoryDomain, MemoryDomainAllocateTest, testing::Combine(kNamedDeviceChoices, kTestOperandTypeChoices), printMemoryDomainAllocateTest); class MemoryDomainCopyTestBase : public MemoryDomainTestBase { protected: MemoryDomainCopyTestBase(sp device, TestOperandType type) : MemoryDomainTestBase(std::move(device), type) {} // Allocates device memory for roles of a single prepared model. // Returns {IBuffer, token} if success; returns {nullptr, 0} if not supported. std::pair, uint32_t> allocateBuffer(const sp& preparedModel, const std::vector& inputIndexes, const std::vector& outputIndexes, const std::vector& dimensions) { if (preparedModel == nullptr) { return {nullptr, 0}; } hidl_vec inputRoles(inputIndexes.size()), outputRoles(outputIndexes.size()); auto trans = [](uint32_t ind) -> BufferRole { return {.modelIndex = 0, .ioIndex = ind, .frequency = 1.0f}; }; std::transform(inputIndexes.begin(), inputIndexes.end(), inputRoles.begin(), trans); std::transform(outputIndexes.begin(), outputIndexes.end(), outputRoles.begin(), trans); sp buffer; uint32_t token = 0; const auto ret = kDevice->allocate( {.dimensions = dimensions}, {preparedModel}, std::move(inputRoles), std::move(outputRoles), [&buffer, &token](ErrorStatus err, const sp& buf, uint32_t tok) { if (err == ErrorStatus::NONE) { EXPECT_NE(buf, nullptr); EXPECT_GT(tok, 0); buffer = buf; token = tok; } else { EXPECT_EQ(err, ErrorStatus::GENERAL_FAILURE); EXPECT_EQ(buf, nullptr); EXPECT_EQ(tok, 0); } }); EXPECT_TRUE(ret.isOk()); return {std::move(buffer), token}; } std::pair, uint32_t> allocateBuffer(const sp& preparedModel, const std::vector& inputIndexes, const std::vector& outputIndexes) { return allocateBuffer(preparedModel, inputIndexes, outputIndexes, {}); } hidl_memory allocateSharedMemory(uint32_t size) { hidl_memory memory = nn::allocateSharedMemory(size); EXPECT_EQ(memory.size(), size); return memory; } void testCopyFrom(const sp& buffer, const hidl_memory& memory, const std::vector& dimensions, ErrorStatus expectedStatus) { const auto ret = buffer->copyFrom(memory, dimensions); ASSERT_TRUE(ret.isOk()); ASSERT_EQ(static_cast(ret), expectedStatus); } void testCopyTo(const sp& buffer, const hidl_memory& memory, ErrorStatus expectedStatus) { const auto ret = buffer->copyTo(memory); ASSERT_TRUE(ret.isOk()); ASSERT_EQ(static_cast(ret), expectedStatus); } void initializeDeviceMemory(const sp& buffer) { hidl_memory memory = nn::allocateSharedMemory(kTestOperandDataSize); ASSERT_EQ(memory.size(), kTestOperandDataSize); testCopyFrom(buffer, memory, kTestOperand.dimensions, ErrorStatus::NONE); } }; using MemoryDomainCopyTestParam = std::tuple; class MemoryDomainCopyTest : public MemoryDomainCopyTestBase, public testing::WithParamInterface { protected: MemoryDomainCopyTest() : MemoryDomainCopyTestBase(getData(std::get(GetParam())), std::get(GetParam())) {} }; TEST_P(MemoryDomainCopyTest, CopyFrom_InvalidMemorySize) { auto preparedModel = createConvPreparedModel(kTestOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}); if (buffer == nullptr) return; uint32_t badMemorySize1 = kTestOperandDataSize / 2, badMemorySize2 = kTestOperandDataSize * 2; hidl_memory badMemory1 = allocateSharedMemory(badMemorySize1); hidl_memory badMemory2 = allocateSharedMemory(badMemorySize2); testCopyFrom(buffer, badMemory1, {}, ErrorStatus::INVALID_ARGUMENT); testCopyFrom(buffer, badMemory2, {}, ErrorStatus::INVALID_ARGUMENT); } TEST_P(MemoryDomainCopyTest, CopyFrom_InvalidMemorySize_DynamicShape) { TestOperand testOperand = kTestOperand; testOperand.dimensions[0] = 0; auto preparedModel = createConvPreparedModel(testOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}); if (buffer == nullptr) return; uint32_t badMemorySize1 = kTestOperandDataSize / 2, badMemorySize2 = kTestOperandDataSize * 2; hidl_memory badMemory1 = allocateSharedMemory(badMemorySize1); hidl_memory badMemory2 = allocateSharedMemory(badMemorySize2); hidl_memory goodMemory = allocateSharedMemory(kTestOperandDataSize); auto badDimensions = kTestOperand.dimensions; badDimensions[0] = 2; testCopyFrom(buffer, badMemory1, kTestOperand.dimensions, ErrorStatus::INVALID_ARGUMENT); testCopyFrom(buffer, badMemory2, kTestOperand.dimensions, ErrorStatus::INVALID_ARGUMENT); testCopyFrom(buffer, goodMemory, kTestOperand.dimensions, ErrorStatus::NONE); testCopyFrom(buffer, goodMemory, badDimensions, ErrorStatus::INVALID_ARGUMENT); } TEST_P(MemoryDomainCopyTest, CopyFrom_InvalidDimensions) { auto preparedModel = createConvPreparedModel(kTestOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}); if (buffer == nullptr) return; hidl_memory memory = allocateSharedMemory(kTestOperandDataSize); std::vector badDimensions; badDimensions = kTestOperand.dimensions; badDimensions.pop_back(); testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); badDimensions = kTestOperand.dimensions; badDimensions[0] = 2; testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); badDimensions = kTestOperand.dimensions; badDimensions[0] = 0; testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); testCopyFrom(buffer, memory, {}, ErrorStatus::NONE); testCopyFrom(buffer, memory, kTestOperand.dimensions, ErrorStatus::NONE); } TEST_P(MemoryDomainCopyTest, CopyFrom_InvalidDimensions_DynamicShape) { TestOperand testOperand = kTestOperand; testOperand.dimensions[0] = 0; auto preparedModel = createConvPreparedModel(testOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}); if (buffer == nullptr) return; hidl_memory memory = allocateSharedMemory(kTestOperandDataSize); std::vector badDimensions; badDimensions = kTestOperand.dimensions; badDimensions.pop_back(); testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); badDimensions = kTestOperand.dimensions; badDimensions[0] = 2; badDimensions[3] = 4; testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); badDimensions = kTestOperand.dimensions; badDimensions[0] = 1; badDimensions[3] = 0; testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); testCopyFrom(buffer, memory, {}, ErrorStatus::INVALID_ARGUMENT); testCopyFrom(buffer, memory, kTestOperand.dimensions, ErrorStatus::NONE); } TEST_P(MemoryDomainCopyTest, CopyTo_UninitializedMemory) { auto preparedModel = createConvPreparedModel(kTestOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}); if (buffer == nullptr) return; hidl_memory memory = allocateSharedMemory(kTestOperandDataSize); testCopyTo(buffer, memory, ErrorStatus::GENERAL_FAILURE); } TEST_P(MemoryDomainCopyTest, CopyTo_InvalidMemorySize) { auto preparedModel = createConvPreparedModel(kTestOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}); if (buffer == nullptr) return; uint32_t badMemorySize1 = kTestOperandDataSize / 2, badMemorySize2 = kTestOperandDataSize * 2; hidl_memory badMemory1 = allocateSharedMemory(badMemorySize1); hidl_memory badMemory2 = allocateSharedMemory(badMemorySize2); hidl_memory goodMemory = allocateSharedMemory(kTestOperandDataSize); initializeDeviceMemory(buffer); testCopyTo(buffer, badMemory1, ErrorStatus::INVALID_ARGUMENT); testCopyTo(buffer, badMemory2, ErrorStatus::INVALID_ARGUMENT); testCopyTo(buffer, goodMemory, ErrorStatus::NONE); } TEST_P(MemoryDomainCopyTest, CopyTo_InvalidMemorySize_DynamicShape) { TestOperand testOperand = kTestOperand; testOperand.dimensions[0] = 0; auto preparedModel = createConvPreparedModel(testOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}); if (buffer == nullptr) return; uint32_t badMemorySize1 = kTestOperandDataSize / 2, badMemorySize2 = kTestOperandDataSize * 2; hidl_memory badMemory1 = allocateSharedMemory(badMemorySize1); hidl_memory badMemory2 = allocateSharedMemory(badMemorySize2); hidl_memory goodMemory = allocateSharedMemory(kTestOperandDataSize); initializeDeviceMemory(buffer); testCopyTo(buffer, badMemory1, ErrorStatus::INVALID_ARGUMENT); testCopyTo(buffer, badMemory2, ErrorStatus::INVALID_ARGUMENT); testCopyTo(buffer, goodMemory, ErrorStatus::NONE); } std::string printMemoryDomainCopyTest( const testing::TestParamInfo& info) { const auto& [namedDevice, operandType] = info.param; const std::string type = toString(static_cast(operandType)); return gtestCompliantName(getName(namedDevice) + "_" + type); } GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MemoryDomainCopyTest); INSTANTIATE_TEST_SUITE_P(TestMemoryDomain, MemoryDomainCopyTest, testing::Combine(kNamedDeviceChoices, kTestOperandTypeChoices), printMemoryDomainCopyTest); using MemoryDomainExecutionTestParam = std::tuple; class MemoryDomainExecutionTest : public MemoryDomainCopyTestBase, public testing::WithParamInterface { protected: MemoryDomainExecutionTest() : MemoryDomainCopyTestBase(getData(std::get(GetParam())), std::get(GetParam())) {} Request::MemoryPool createSharedMemoryPool(uint32_t size) { hidl_memory memory = allocateSharedMemory(size); Request::MemoryPool pool; pool.hidlMemory(memory); return pool; } Request::MemoryPool createDeviceMemoryPool(uint32_t token) { Request::MemoryPool pool; pool.token(token); return pool; } void testExecution(const sp& preparedModel, const Request& request, ErrorStatus expectedStatus) { switch (kExecutor) { case Executor::ASYNC: EXPECT_EQ(executeAsync(preparedModel, request), expectedStatus); break; case Executor::SYNC: EXPECT_EQ(executeSync(preparedModel, request), expectedStatus); break; case Executor::FENCED: EXPECT_EQ(executeFenced(preparedModel, request), expectedStatus); break; default: ASSERT_TRUE(false); } } ErrorStatus executeAsync(const sp& preparedModel, const Request& request) { ErrorStatus executionStatus; // launch execution sp executionCallback = new ExecutionCallback(); const auto ret = preparedModel->execute_1_3(request, MeasureTiming::NO, {}, {}, executionCallback); EXPECT_TRUE(ret.isOk()); executionStatus = static_cast(ret); // retrieve execution status executionCallback->wait(); if (executionStatus == ErrorStatus::NONE) { executionStatus = executionCallback->getStatus(); } else { EXPECT_EQ(executionStatus, executionCallback->getStatus()); } const auto timing = executionCallback->getTiming(); EXPECT_EQ(UINT64_MAX, timing.timeOnDevice); EXPECT_EQ(UINT64_MAX, timing.timeInDriver); if (executionStatus != ErrorStatus::NONE) { EXPECT_EQ(executionCallback->getOutputShapes().size(), 0); } return executionStatus; } ErrorStatus executeSync(const sp& preparedModel, const Request& request) { ErrorStatus executionStatus; const auto ret = preparedModel->executeSynchronously_1_3( request, MeasureTiming::NO, {}, {}, [&executionStatus](ErrorStatus error, const hidl_vec& shapes, const Timing& time) { executionStatus = error; EXPECT_EQ(UINT64_MAX, time.timeOnDevice); EXPECT_EQ(UINT64_MAX, time.timeInDriver); if (executionStatus != ErrorStatus::NONE) { EXPECT_EQ(shapes.size(), 0); } }); EXPECT_TRUE(ret.isOk()); return executionStatus; } ErrorStatus executeFenced(const sp& preparedModel, const Request& request) { ErrorStatus executionStatus; hidl_handle syncFenceHandle; sp fencedCallback; const auto callbackFunc = [&executionStatus, &syncFenceHandle, &fencedCallback]( ErrorStatus error, const hidl_handle& handle, const sp& callback) { executionStatus = error; syncFenceHandle = handle; fencedCallback = callback; }; Return ret = preparedModel->executeFenced(request, {}, MeasureTiming::NO, {}, {}, {}, callbackFunc); EXPECT_TRUE(ret.isOk()); if (executionStatus != ErrorStatus::NONE) { EXPECT_EQ(syncFenceHandle.getNativeHandle(), nullptr); EXPECT_EQ(fencedCallback, nullptr); return executionStatus; } if (syncFenceHandle.getNativeHandle()) { waitForSyncFence(syncFenceHandle.getNativeHandle()->data[0]); } EXPECT_NE(fencedCallback, nullptr); ret = fencedCallback->getExecutionInfo( [&executionStatus](ErrorStatus error, Timing t, Timing) { executionStatus = error; EXPECT_EQ(UINT64_MAX, t.timeOnDevice); EXPECT_EQ(UINT64_MAX, t.timeInDriver); }); EXPECT_TRUE(ret.isOk()); return executionStatus; } const Executor kExecutor = std::get(GetParam()); }; TEST_P(MemoryDomainExecutionTest, InvalidToken) { auto preparedModel = createConvPreparedModel(kTestOperand); if (preparedModel == nullptr) return; Request::MemoryPool sharedMemory = createSharedMemoryPool(kTestOperandDataSize); Request::MemoryPool badDeviceMemory1 = createDeviceMemoryPool(0); // Invalid token. Request::MemoryPool badDeviceMemory2 = createDeviceMemoryPool(100); // Unknown token. RequestArgument sharedMemoryArg = { .location = {.poolIndex = 0, .offset = 0, .length = kTestOperandDataSize}}; RequestArgument deviceMemoryArg = {.location = {.poolIndex = 1}}; testExecution(preparedModel, {.inputs = {deviceMemoryArg}, .outputs = {sharedMemoryArg}, .pools = {sharedMemory, badDeviceMemory1}}, ErrorStatus::INVALID_ARGUMENT); testExecution(preparedModel, {.inputs = {deviceMemoryArg}, .outputs = {sharedMemoryArg}, .pools = {sharedMemory, badDeviceMemory2}}, ErrorStatus::INVALID_ARGUMENT); testExecution(preparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArg}, .pools = {sharedMemory, badDeviceMemory1}}, ErrorStatus::INVALID_ARGUMENT); testExecution(preparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArg}, .pools = {sharedMemory, badDeviceMemory2}}, ErrorStatus::INVALID_ARGUMENT); } TEST_P(MemoryDomainExecutionTest, InvalidPreparedModel) { auto preparedModel = createConvPreparedModel(kTestOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}); if (buffer == nullptr) return; auto badPreparedModel = createConvPreparedModel(kTestOperand); if (badPreparedModel == nullptr) return; Request::MemoryPool sharedMemory = createSharedMemoryPool(kTestOperandDataSize); Request::MemoryPool deviceMemory = createDeviceMemoryPool(token); RequestArgument sharedMemoryArg = { .location = {.poolIndex = 0, .offset = 0, .length = kTestOperandDataSize}}; RequestArgument deviceMemoryArg = {.location = {.poolIndex = 1}}; // This should fail, because the buffer is not allocated for badPreparedModel. initializeDeviceMemory(buffer); testExecution(badPreparedModel, {.inputs = {deviceMemoryArg}, .outputs = {sharedMemoryArg}, .pools = {sharedMemory, deviceMemory}}, ErrorStatus::INVALID_ARGUMENT); testExecution(badPreparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArg}, .pools = {sharedMemory, deviceMemory}}, ErrorStatus::INVALID_ARGUMENT); } TEST_P(MemoryDomainExecutionTest, InvalidIOIndex) { auto preparedModel = createConvPreparedModel(kTestOperand, 2); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {}); if (buffer == nullptr) return; Request::MemoryPool sharedMemory1 = createSharedMemoryPool(kTestOperandDataSize); Request::MemoryPool sharedMemory2 = createSharedMemoryPool(kTestOperandDataSize); Request::MemoryPool sharedMemory3 = createSharedMemoryPool(kTestOperandDataSize); Request::MemoryPool deviceMemory = createDeviceMemoryPool(token); RequestArgument sharedMemoryArg1 = { .location = {.poolIndex = 0, .offset = 0, .length = kTestOperandDataSize}}; RequestArgument sharedMemoryArg2 = { .location = {.poolIndex = 1, .offset = 0, .length = kTestOperandDataSize}}; RequestArgument sharedMemoryArg3 = { .location = {.poolIndex = 2, .offset = 0, .length = kTestOperandDataSize}}; RequestArgument deviceMemoryArg = {.location = {.poolIndex = 3}}; // This should fail, because the device memory is not allocated for input 1. initializeDeviceMemory(buffer); testExecution(preparedModel, {.inputs = {sharedMemoryArg1, deviceMemoryArg}, .outputs = {sharedMemoryArg2, sharedMemoryArg3}, .pools = {sharedMemory1, sharedMemory2, sharedMemory3, deviceMemory}}, ErrorStatus::INVALID_ARGUMENT); // This should fail, because the device memory is not allocated for output 1. testExecution(preparedModel, {.inputs = {sharedMemoryArg1, sharedMemoryArg2}, .outputs = {sharedMemoryArg3, deviceMemoryArg}, .pools = {sharedMemory1, sharedMemory2, sharedMemory3, deviceMemory}}, ErrorStatus::INVALID_ARGUMENT); } TEST_P(MemoryDomainExecutionTest, InvalidIOType) { auto preparedModel = createConvPreparedModel(kTestOperand); auto [inputBuffer, inputToken] = allocateBuffer(preparedModel, {0}, {}); auto [outputBuffer, outputToken] = allocateBuffer(preparedModel, {}, {0}); if (inputBuffer == nullptr || outputBuffer == nullptr) return; Request::MemoryPool sharedMemory = createSharedMemoryPool(kTestOperandDataSize); Request::MemoryPool deviceMemory = createDeviceMemoryPool(inputToken); RequestArgument sharedMemoryArg = { .location = {.poolIndex = 0, .offset = 0, .length = kTestOperandDataSize}}; RequestArgument deviceMemoryArg = {.location = {.poolIndex = 1}}; // This should fail, because the device memory is allocated for input but used as output. testExecution(preparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArg}, .pools = {sharedMemory, deviceMemory}}, ErrorStatus::INVALID_ARGUMENT); // This should fail, because the device memory is allocated for output but used as input. deviceMemory.token(outputToken); initializeDeviceMemory(outputBuffer); testExecution(preparedModel, {.inputs = {deviceMemoryArg}, .outputs = {sharedMemoryArg}, .pools = {sharedMemory, deviceMemory}}, ErrorStatus::INVALID_ARGUMENT); } TEST_P(MemoryDomainExecutionTest, UninitializedMemory) { auto preparedModel = createConvPreparedModel(kTestOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}); if (buffer == nullptr) return; Request::MemoryPool sharedMemory = createSharedMemoryPool(kTestOperandDataSize); Request::MemoryPool deviceMemory = createDeviceMemoryPool(token); RequestArgument sharedMemoryArg = { .location = {.poolIndex = 0, .offset = 0, .length = kTestOperandDataSize}}; RequestArgument deviceMemoryArg = {.location = {.poolIndex = 1}}; // This should fail, because the device memory is not initialized. testExecution(preparedModel, {.inputs = {deviceMemoryArg}, .outputs = {sharedMemoryArg}, .pools = {sharedMemory, deviceMemory}}, ErrorStatus::GENERAL_FAILURE); // This should initialize the device memory. testExecution(preparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArg}, .pools = {sharedMemory, deviceMemory}}, ErrorStatus::NONE); // Test again with initialized device memory. testExecution(preparedModel, {.inputs = {deviceMemoryArg}, .outputs = {sharedMemoryArg}, .pools = {sharedMemory, deviceMemory}}, ErrorStatus::NONE); } TEST_P(MemoryDomainExecutionTest, SameRequestMultipleRoles) { auto preparedModel = createConvPreparedModel(kTestOperand, 2); auto [buffer, token] = allocateBuffer(preparedModel, {0, 1}, {0, 1}); if (buffer == nullptr) return; Request::MemoryPool sharedMemory1 = createSharedMemoryPool(kTestOperandDataSize); Request::MemoryPool sharedMemory2 = createSharedMemoryPool(kTestOperandDataSize); Request::MemoryPool deviceMemory = createDeviceMemoryPool(token); RequestArgument sharedMemoryArg1 = { .location = {.poolIndex = 0, .offset = 0, .length = kTestOperandDataSize}}; RequestArgument sharedMemoryArg2 = { .location = {.poolIndex = 1, .offset = 0, .length = kTestOperandDataSize}}; RequestArgument deviceMemoryArg = {.location = {.poolIndex = 2}}; // This should fail, because the same device memory cannot be used for both input and output. initializeDeviceMemory(buffer); testExecution(preparedModel, {.inputs = {deviceMemoryArg, sharedMemoryArg1}, .outputs = {deviceMemoryArg, sharedMemoryArg2}, .pools = {sharedMemory1, sharedMemory2, deviceMemory}}, ErrorStatus::INVALID_ARGUMENT); // This should fail, because the same device memory cannot be used for multiple outputs. testExecution(preparedModel, {.inputs = {sharedMemoryArg1, sharedMemoryArg2}, .outputs = {deviceMemoryArg, deviceMemoryArg}, .pools = {sharedMemory1, sharedMemory2, deviceMemory}}, ErrorStatus::INVALID_ARGUMENT); // The same device memory can be used for multiple inputs. initializeDeviceMemory(buffer); testExecution(preparedModel, {.inputs = {deviceMemoryArg, deviceMemoryArg}, .outputs = {sharedMemoryArg1, sharedMemoryArg2}, .pools = {sharedMemory1, sharedMemory2, deviceMemory}}, ErrorStatus::NONE); } TEST_P(MemoryDomainExecutionTest, InvalidDimensions) { // FENCED execution does not support dynamic shape. if (kExecutor == Executor::FENCED) return; TestOperand testOperand = kTestOperand; testOperand.dimensions[0] = 0; auto preparedModel = createConvPreparedModel(testOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}, kTestOperand.dimensions); if (buffer == nullptr) return; // Use an incompatible dimension and make sure the length matches with the bad dimension. auto badDimensions = kTestOperand.dimensions; badDimensions[0] = 2; const uint32_t badTestOperandDataSize = kTestOperandDataSize * 2; Request::MemoryPool sharedMemory = createSharedMemoryPool(badTestOperandDataSize); Request::MemoryPool deviceMemory = createDeviceMemoryPool(token); RequestArgument sharedMemoryArg = { .location = {.poolIndex = 0, .offset = 0, .length = badTestOperandDataSize}, .dimensions = badDimensions}; RequestArgument deviceMemoryArg = {.location = {.poolIndex = 1}}; RequestArgument deviceMemoryArgWithBadDimensions = {.location = {.poolIndex = 1}, .dimensions = badDimensions}; initializeDeviceMemory(buffer); testExecution(preparedModel, {.inputs = {deviceMemoryArgWithBadDimensions}, .outputs = {sharedMemoryArg}, .pools = {sharedMemory, deviceMemory}}, ErrorStatus::INVALID_ARGUMENT); testExecution(preparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArgWithBadDimensions}, .pools = {sharedMemory, deviceMemory}}, ErrorStatus::INVALID_ARGUMENT); testExecution(preparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArg}, .pools = {sharedMemory, deviceMemory}}, ErrorStatus::GENERAL_FAILURE); } const auto kExecutorChoices = testing::Values(Executor::ASYNC, Executor::SYNC, Executor::FENCED); std::string printMemoryDomainExecutionTest( const testing::TestParamInfo& info) { const auto& [namedDevice, operandType, executor] = info.param; const std::string type = toString(static_cast(operandType)); const std::string executorStr = toString(executor); return gtestCompliantName(getName(namedDevice) + "_" + type + "_" + executorStr); } GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MemoryDomainExecutionTest); INSTANTIATE_TEST_SUITE_P(TestMemoryDomain, MemoryDomainExecutionTest, testing::Combine(kNamedDeviceChoices, kTestOperandTypeChoices, kExecutorChoices), printMemoryDomainExecutionTest); } // namespace android::hardware::neuralnetworks::V1_3::vts::functional