/* * Copyright (C) 2021 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_aidl_hal_test" #include #include #include #include #include #include #include #include #include #include #include #include #include "AidlHalInterfaces.h" #include "Callbacks.h" #include "GeneratedTestHarness.h" #include "MemoryUtils.h" #include "Utils.h" #include "VtsHalNeuralnetworks.h" namespace aidl::android::hardware::neuralnetworks::vts::functional { using namespace test_helper; using implementation::PreparedModelCallback; namespace { // An AIDL 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); // TODO(b/179270601): restore kNamedDeviceChoices 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 placeholder 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 placeholder invalid IPreparedModel class for MemoryDomainAllocateTest.InvalidPreparedModel class InvalidPreparedModel : public BnPreparedModel { public: ndk::ScopedAStatus executeSynchronously(const Request&, bool, int64_t, int64_t, ExecutionResult*) override { return ndk::ScopedAStatus::fromServiceSpecificError( static_cast(ErrorStatus::GENERAL_FAILURE)); } ndk::ScopedAStatus executeFenced(const Request&, const std::vector&, bool, int64_t, int64_t, int64_t, FencedExecutionResult*) override { return ndk::ScopedAStatus::fromServiceSpecificError( static_cast(ErrorStatus::GENERAL_FAILURE)); } ndk::ScopedAStatus configureExecutionBurst(std::shared_ptr*) override { return ndk::ScopedAStatus::fromServiceSpecificError( static_cast(ErrorStatus::GENERAL_FAILURE)); } }; template std::vector createRequestMemoryPools(const Args&... pools) { std::vector memoryPools; memoryPools.reserve(sizeof...(Args)); // This fold operator calls push_back on each of the function arguments. (memoryPools.push_back(utils::clone(pools).value()), ...); return memoryPools; }; } // namespace class MemoryDomainTestBase : public testing::Test { protected: MemoryDomainTestBase(std::shared_ptr 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 = ndk::ScopedAStatus::fromStatus(AIBinder_ping(kDevice->asBinder().get())).isOk(); ASSERT_TRUE(deviceIsResponsive); } std::shared_ptr createConvPreparedModel(const TestOperand& testOperand, uint32_t numOperations = 1) { const TestModel testModel = createConvModel(testOperand, numOperations); const Model model = createModel(testModel); std::shared_ptr preparedModel; createPreparedModel(kDevice, model, &preparedModel, /*reportSkipping=*/false); return preparedModel; } std::shared_ptr createAddPreparedModel(const TestOperand& testOperand) { const TestModel testModel = createSingleAddModel(testOperand); const Model model = createModel(testModel); std::shared_ptr preparedModel; createPreparedModel(kDevice, model, &preparedModel, /*reportSkipping=*/false); return preparedModel; } static const std::map kTestOperandMap; const std::shared_ptr 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 { std::vector dimensions; std::vector> preparedModels; std::vector inputRoles; std::vector 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) { std::vector preparedModelParcels; preparedModelParcels.reserve(args.preparedModels.size()); for (const auto& model : args.preparedModels) { preparedModelParcels.push_back({.preparedModel = model}); } DeviceBuffer buffer; const auto ret = kDevice->allocate({.dimensions = std::move(args.dimensions)}, preparedModelParcels, args.inputRoles, args.outputRoles, &buffer); ASSERT_EQ(ret.getExceptionCode(), EX_SERVICE_SPECIFIC); ASSERT_TRUE(static_cast(ret.getServiceSpecificError()) == ErrorStatus::INVALID_ARGUMENT || static_cast(ret.getServiceSpecificError()) == ErrorStatus::GENERAL_FAILURE); } void testConflictOperands(const std::shared_ptr& model1, const std::shared_ptr& model2) { validateAllocate({ .preparedModels = {model1, model2}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}, {.modelIndex = 1, .ioIndex = 0, .probability = 1.0f}}, }); validateAllocate({ .preparedModels = {model1, model2}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}}, .outputRoles = {{.modelIndex = 1, .ioIndex = 0, .probability = 1.0f}}, }); validateAllocate({ .preparedModels = {model1, model2}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}, {.modelIndex = 1, .ioIndex = 0, .probability = 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, .probability = 1.0f}}, }); // Test with nullptr prepared model as output role. validateAllocate({ .preparedModels = {nullptr}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}}, }); } TEST_P(MemoryDomainAllocateTest, InvalidPreparedModel) { std::shared_ptr invalidPreparedModel = ndk::SharedRefBase::make(); // Test with invalid prepared model as input role. validateAllocate({ .preparedModels = {invalidPreparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}}, }); // Test with invalid prepared model as output role. validateAllocate({ .preparedModels = {invalidPreparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 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, .probability = 1.0f}}, }); // This should fail, because the model index is out of bound. validateAllocate({ .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 1, .ioIndex = 0, .probability = 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, .probability = 1.0f}}, }); // This should fail, because the model only has one output. validateAllocate({ .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 1, .probability = 1.0f}}, }); } TEST_P(MemoryDomainAllocateTest, InvalidProbability) { auto preparedModel = createConvPreparedModel(kTestOperand); if (preparedModel == nullptr) return; for (float invalidFreq : {10.0f, 0.0f, -0.5f}) { // Test with invalid probability for input roles. validateAllocate({ .preparedModels = {preparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = invalidFreq}}, }); // Test with invalid probability for output roles. validateAllocate({ .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 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, .probability = 1.0f}, {.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}}, }); validateAllocate({ .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}, {.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}}, }); // Different model indexes, but logically referring to the same role. validateAllocate({ .preparedModels = {preparedModel, preparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}, {.modelIndex = 1, .ioIndex = 0, .probability = 1.0f}}, }); validateAllocate({ .preparedModels = {preparedModel, preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}, {.modelIndex = 1, .ioIndex = 0, .probability = 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 = utils::toSigned(kTestOperand.dimensions).value(); badDimensions.pop_back(); validateAllocate({ .dimensions = badDimensions, .preparedModels = {preparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}}, }); validateAllocate({ .dimensions = badDimensions, .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}}, }); } TEST_P(MemoryDomainAllocateTest, ConflictDimensionsBetweenRoleAndDesc) { auto preparedModel = createConvPreparedModel(kTestOperand); if (preparedModel == nullptr) return; auto badDimensions = utils::toSigned(kTestOperand.dimensions).value(); badDimensions[0] = 4; validateAllocate({ .dimensions = badDimensions, .preparedModels = {preparedModel}, .inputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 1.0f}}, }); validateAllocate({ .dimensions = badDimensions, .preparedModels = {preparedModel}, .outputRoles = {{.modelIndex = 0, .ioIndex = 0, .probability = 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, .probability = 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(testing::ValuesIn(getNamedDevices()), kTestOperandTypeChoices), printMemoryDomainAllocateTest); class MemoryDomainCopyTestBase : public MemoryDomainTestBase { protected: MemoryDomainCopyTestBase(std::shared_ptr 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. DeviceBuffer allocateBuffer(const std::shared_ptr& preparedModel, const std::vector& inputIndexes, const std::vector& outputIndexes, const std::vector& dimensions) { if (preparedModel == nullptr) { return {.buffer = nullptr, .token = 0}; } std::vector inputRoles(inputIndexes.size()), outputRoles(outputIndexes.size()); auto trans = [](int32_t ind) -> BufferRole { return {.modelIndex = 0, .ioIndex = ind, .probability = 1.0f}; }; std::transform(inputIndexes.begin(), inputIndexes.end(), inputRoles.begin(), trans); std::transform(outputIndexes.begin(), outputIndexes.end(), outputRoles.begin(), trans); IPreparedModelParcel parcel; parcel.preparedModel = preparedModel; DeviceBuffer buffer; const auto ret = kDevice->allocate({.dimensions = dimensions}, {parcel}, inputRoles, outputRoles, &buffer); if (!ret.isOk()) { EXPECT_EQ(ret.getExceptionCode(), EX_SERVICE_SPECIFIC); EXPECT_EQ(static_cast(ret.getServiceSpecificError()), ErrorStatus::GENERAL_FAILURE); return DeviceBuffer{ .buffer = nullptr, .token = 0, }; } EXPECT_NE(buffer.buffer, nullptr); EXPECT_GT(buffer.token, 0); return buffer; } DeviceBuffer allocateBuffer(const std::shared_ptr& preparedModel, const std::vector& inputIndexes, const std::vector& outputIndexes) { return allocateBuffer(preparedModel, inputIndexes, outputIndexes, {}); } size_t getSize(const Memory& memory) { switch (memory.getTag()) { case Memory::Tag::ashmem: return memory.get().size; case Memory::Tag::mappableFile: return memory.get().length; case Memory::Tag::hardwareBuffer: { const auto& hardwareBuffer = memory.get(); const bool isBlob = hardwareBuffer.description.format == graphics::common::PixelFormat::BLOB; return isBlob ? hardwareBuffer.description.width : 0; } } return 0; } Memory allocateSharedMemory(uint32_t size) { const auto sharedMemory = nn::createSharedMemory(size).value(); auto memory = utils::convert(sharedMemory).value(); EXPECT_EQ(getSize(memory), size); return memory; } void testCopyFrom(const std::shared_ptr& buffer, const Memory& memory, const std::vector& dimensions, ErrorStatus expectedStatus) { const auto ret = buffer->copyFrom(memory, dimensions); if (expectedStatus == ErrorStatus::NONE) { ASSERT_TRUE(ret.isOk()); } else { ASSERT_EQ(ret.getExceptionCode(), EX_SERVICE_SPECIFIC); ASSERT_EQ(expectedStatus, static_cast(ret.getServiceSpecificError())); } } void testCopyTo(const std::shared_ptr& buffer, const Memory& memory, ErrorStatus expectedStatus) { const auto ret = buffer->copyTo(memory); if (expectedStatus == ErrorStatus::NONE) { ASSERT_TRUE(ret.isOk()); } else { ASSERT_EQ(ret.getExceptionCode(), EX_SERVICE_SPECIFIC); ASSERT_EQ(expectedStatus, static_cast(ret.getServiceSpecificError())); } } void initializeDeviceMemory(const std::shared_ptr& buffer) { Memory memory = allocateSharedMemory(kTestOperandDataSize); ASSERT_EQ(getSize(memory), kTestOperandDataSize); testCopyFrom(buffer, memory, utils::toSigned(kTestOperand.dimensions).value(), 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; Memory badMemory1 = allocateSharedMemory(badMemorySize1); 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; Memory badMemory1 = allocateSharedMemory(badMemorySize1); Memory badMemory2 = allocateSharedMemory(badMemorySize2); Memory goodMemory = allocateSharedMemory(kTestOperandDataSize); const auto goodDimensions = utils::toSigned(kTestOperand.dimensions).value(); auto badDimensions = goodDimensions; badDimensions[0] = 2; testCopyFrom(buffer, badMemory1, goodDimensions, ErrorStatus::INVALID_ARGUMENT); testCopyFrom(buffer, badMemory2, goodDimensions, ErrorStatus::INVALID_ARGUMENT); testCopyFrom(buffer, goodMemory, goodDimensions, 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; Memory memory = allocateSharedMemory(kTestOperandDataSize); const auto goodDimensions = utils::toSigned(kTestOperand.dimensions).value(); std::vector badDimensions = goodDimensions; badDimensions.pop_back(); testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); badDimensions = goodDimensions; badDimensions[0] = 2; testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); badDimensions = goodDimensions; badDimensions[0] = 0; testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); testCopyFrom(buffer, memory, {}, ErrorStatus::NONE); testCopyFrom(buffer, memory, goodDimensions, 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; Memory memory = allocateSharedMemory(kTestOperandDataSize); const auto goodDimensions = utils::toSigned(kTestOperand.dimensions).value(); std::vector badDimensions = goodDimensions; badDimensions.pop_back(); testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); badDimensions = goodDimensions; badDimensions[0] = 2; badDimensions[3] = 4; testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); badDimensions = goodDimensions; badDimensions[0] = 1; badDimensions[3] = 0; testCopyFrom(buffer, memory, badDimensions, ErrorStatus::INVALID_ARGUMENT); testCopyFrom(buffer, memory, {}, ErrorStatus::INVALID_ARGUMENT); testCopyFrom(buffer, memory, goodDimensions, ErrorStatus::NONE); } TEST_P(MemoryDomainCopyTest, CopyTo_UninitializedMemory) { auto preparedModel = createConvPreparedModel(kTestOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}); if (buffer == nullptr) return; 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; Memory badMemory1 = allocateSharedMemory(badMemorySize1); Memory badMemory2 = allocateSharedMemory(badMemorySize2); 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; Memory badMemory1 = allocateSharedMemory(badMemorySize1); Memory badMemory2 = allocateSharedMemory(badMemorySize2); 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(testing::ValuesIn(getNamedDevices()), kTestOperandTypeChoices), printMemoryDomainCopyTest); using MemoryDomainExecutionTestParam = std::tuple; class MemoryDomainExecutionTest : public MemoryDomainCopyTestBase, public testing::WithParamInterface { protected: MemoryDomainExecutionTest() : MemoryDomainCopyTestBase(getData(std::get(GetParam())), std::get(GetParam())) {} RequestMemoryPool createSharedMemoryPool(uint32_t size) { return RequestMemoryPool(allocateSharedMemory(size)); } RequestMemoryPool createDeviceMemoryPool(uint32_t token) { return RequestMemoryPool(static_cast(token)); } void testExecution(const std::shared_ptr& preparedModel, const Request& request, ErrorStatus expectedStatus) { switch (kExecutor) { case Executor::SYNC: EXPECT_EQ(executeSync(preparedModel, request), expectedStatus); break; case Executor::BURST: EXPECT_EQ(executeBurst(preparedModel, request), expectedStatus); break; case Executor::FENCED: EXPECT_EQ(executeFenced(preparedModel, request), expectedStatus); break; default: ASSERT_TRUE(false); } } ErrorStatus executeSync(const std::shared_ptr& preparedModel, const Request& request) { ExecutionResult executionResult; const auto ret = preparedModel->executeSynchronously( request, false, kNoDeadline, kOmittedTimeoutDuration, &executionResult); if (!ret.isOk()) { EXPECT_EQ(ret.getExceptionCode(), EX_SERVICE_SPECIFIC); return static_cast(ret.getServiceSpecificError()); } const ErrorStatus executionStatus = executionResult.outputSufficientSize ? ErrorStatus::NONE : ErrorStatus::OUTPUT_INSUFFICIENT_SIZE; EXPECT_EQ(executionResult.timing, kNoTiming); return executionStatus; } ErrorStatus executeFenced(const std::shared_ptr& preparedModel, const Request& request) { FencedExecutionResult executionResult; const auto ret = preparedModel->executeFenced(request, {}, false, kNoDeadline, kOmittedTimeoutDuration, kNoDuration, &executionResult); if (!ret.isOk()) { EXPECT_EQ(ret.getExceptionCode(), EX_SERVICE_SPECIFIC); return static_cast(ret.getServiceSpecificError()); } if (executionResult.syncFence.get() != -1) { waitForSyncFence(executionResult.syncFence.get()); } EXPECT_NE(executionResult.callback, nullptr); ErrorStatus executionStatus = ErrorStatus::GENERAL_FAILURE; Timing time = kNoTiming; Timing timeFenced = kNoTiming; const auto retExecutionInfo = executionResult.callback->getExecutionInfo(&time, &timeFenced, &executionStatus); EXPECT_TRUE(retExecutionInfo.isOk()); EXPECT_EQ(time, kNoTiming); return executionStatus; } ErrorStatus executeBurst(const std::shared_ptr& preparedModel, const Request& request) { // create burst std::shared_ptr burst; auto ret = preparedModel->configureExecutionBurst(&burst); EXPECT_TRUE(ret.isOk()) << ret.getDescription(); EXPECT_NE(nullptr, burst.get()); if (!ret.isOk() || burst.get() == nullptr) { return ErrorStatus::GENERAL_FAILURE; } // use -1 for all memory identifier tokens const std::vector slots(request.pools.size(), -1); ExecutionResult executionResult; ret = burst->executeSynchronously(request, slots, false, kNoDeadline, kOmittedTimeoutDuration, &executionResult); if (!ret.isOk()) { EXPECT_EQ(ret.getExceptionCode(), EX_SERVICE_SPECIFIC); return static_cast(ret.getServiceSpecificError()); } const ErrorStatus executionStatus = executionResult.outputSufficientSize ? ErrorStatus::NONE : ErrorStatus::OUTPUT_INSUFFICIENT_SIZE; EXPECT_EQ(executionResult.timing, kNoTiming); return executionStatus; } const Executor kExecutor = std::get(GetParam()); }; TEST_P(MemoryDomainExecutionTest, InvalidToken) { auto preparedModel = createConvPreparedModel(kTestOperand); if (preparedModel == nullptr) return; RequestMemoryPool sharedMemory = createSharedMemoryPool(kTestOperandDataSize); RequestMemoryPool badDeviceMemory1 = createDeviceMemoryPool(0); // Invalid token. RequestMemoryPool 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 = createRequestMemoryPools(sharedMemory, badDeviceMemory1)}, ErrorStatus::INVALID_ARGUMENT); testExecution(preparedModel, {.inputs = {deviceMemoryArg}, .outputs = {sharedMemoryArg}, .pools = createRequestMemoryPools(sharedMemory, badDeviceMemory2)}, ErrorStatus::INVALID_ARGUMENT); testExecution(preparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArg}, .pools = createRequestMemoryPools(sharedMemory, badDeviceMemory1)}, ErrorStatus::INVALID_ARGUMENT); testExecution(preparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArg}, .pools = createRequestMemoryPools(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; RequestMemoryPool sharedMemory = createSharedMemoryPool(kTestOperandDataSize); RequestMemoryPool 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 = createRequestMemoryPools(sharedMemory, deviceMemory)}, ErrorStatus::INVALID_ARGUMENT); testExecution(badPreparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArg}, .pools = createRequestMemoryPools(sharedMemory, deviceMemory)}, ErrorStatus::INVALID_ARGUMENT); } TEST_P(MemoryDomainExecutionTest, InvalidIOIndex) { auto preparedModel = createConvPreparedModel(kTestOperand, 2); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {}); if (buffer == nullptr) return; RequestMemoryPool sharedMemory1 = createSharedMemoryPool(kTestOperandDataSize); RequestMemoryPool sharedMemory2 = createSharedMemoryPool(kTestOperandDataSize); RequestMemoryPool sharedMemory3 = createSharedMemoryPool(kTestOperandDataSize); RequestMemoryPool 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 = createRequestMemoryPools(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 = createRequestMemoryPools(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; RequestMemoryPool sharedMemory = createSharedMemoryPool(kTestOperandDataSize); RequestMemoryPool 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 = createRequestMemoryPools(sharedMemory, deviceMemory)}, ErrorStatus::INVALID_ARGUMENT); // This should fail, because the device memory is allocated for output but used as input. deviceMemory.set(outputToken); initializeDeviceMemory(outputBuffer); testExecution(preparedModel, {.inputs = {deviceMemoryArg}, .outputs = {sharedMemoryArg}, .pools = createRequestMemoryPools(sharedMemory, deviceMemory)}, ErrorStatus::INVALID_ARGUMENT); } TEST_P(MemoryDomainExecutionTest, UninitializedMemory) { auto preparedModel = createConvPreparedModel(kTestOperand); auto [buffer, token] = allocateBuffer(preparedModel, {0}, {0}); if (buffer == nullptr) return; RequestMemoryPool sharedMemory = createSharedMemoryPool(kTestOperandDataSize); RequestMemoryPool 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 = createRequestMemoryPools(sharedMemory, deviceMemory)}, ErrorStatus::GENERAL_FAILURE); // This should initialize the device memory. testExecution(preparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArg}, .pools = createRequestMemoryPools(sharedMemory, deviceMemory)}, ErrorStatus::NONE); // Test again with initialized device memory. testExecution(preparedModel, {.inputs = {deviceMemoryArg}, .outputs = {sharedMemoryArg}, .pools = createRequestMemoryPools(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; RequestMemoryPool sharedMemory1 = createSharedMemoryPool(kTestOperandDataSize); RequestMemoryPool sharedMemory2 = createSharedMemoryPool(kTestOperandDataSize); RequestMemoryPool 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 = createRequestMemoryPools(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 = createRequestMemoryPools(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 = createRequestMemoryPools(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 deviceBuffer = allocateBuffer(preparedModel, {0}, {0}, utils::toSigned(kTestOperand.dimensions).value()); if (deviceBuffer.buffer == nullptr) return; // Use an incompatible dimension and make sure the length matches with the bad dimension. auto badDimensions = utils::toSigned(kTestOperand.dimensions).value(); badDimensions[0] = 2; const uint32_t badTestOperandDataSize = kTestOperandDataSize * 2; RequestMemoryPool sharedMemory = createSharedMemoryPool(badTestOperandDataSize); RequestMemoryPool deviceMemory = createDeviceMemoryPool(deviceBuffer.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(deviceBuffer.buffer); testExecution(preparedModel, {.inputs = {deviceMemoryArgWithBadDimensions}, .outputs = {sharedMemoryArg}, .pools = createRequestMemoryPools(sharedMemory, deviceMemory)}, ErrorStatus::INVALID_ARGUMENT); testExecution(preparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArgWithBadDimensions}, .pools = createRequestMemoryPools(sharedMemory, deviceMemory)}, ErrorStatus::INVALID_ARGUMENT); testExecution(preparedModel, {.inputs = {sharedMemoryArg}, .outputs = {deviceMemoryArg}, .pools = createRequestMemoryPools(sharedMemory, deviceMemory)}, ErrorStatus::GENERAL_FAILURE); } const auto kExecutorChoices = testing::Values(Executor::SYNC, Executor::BURST, 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(testing::ValuesIn(getNamedDevices()), kTestOperandTypeChoices, kExecutorChoices), printMemoryDomainExecutionTest); } // namespace aidl::android::hardware::neuralnetworks::vts::functional